From 5892080f6c0dac60c4318282f2c0b5a63e206a6e Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Wed, 26 Jun 2024 12:23:06 -0600 Subject: [PATCH 1/5] funcionalidad para crear usuario solicitante --- cosiap_api/cosiap_api/urls.py | 2 +- cosiap_api/users/serializers.py | 45 +++++++++++++++++++++++++++++++++ cosiap_api/users/urls.py | 5 +++- cosiap_api/users/views.py | 24 +++++++++++++++++- 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 cosiap_api/users/serializers.py diff --git a/cosiap_api/cosiap_api/urls.py b/cosiap_api/cosiap_api/urls.py index e4f406e..01e0f52 100644 --- a/cosiap_api/cosiap_api/urls.py +++ b/cosiap_api/cosiap_api/urls.py @@ -24,7 +24,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, Spec urlpatterns = [ path('admin/', admin.site.urls), - path('',include('users.urls')), + path('usuarios/',include('users.urls')), path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py new file mode 100644 index 0000000..015e4c7 --- /dev/null +++ b/cosiap_api/users/serializers.py @@ -0,0 +1,45 @@ +# Archivo con los serializers necesarios para la gestión de los usuarios +# Autores: Adalberto Cerrillo Vázquez, +# Versión: 1.0 + + +from rest_framework import serializers +from .models import Usuario + +# serializer para el usuario solicitante +class usuario_solicitante_serializer(serializers.ModelSerializer): + # Creamos un campo de confirmación de la contraseña del solicitante + confirmar_password = serializers.CharField(write_only=True) + + class Meta: + # Indicamos el modelo + model = Usuario + # Indicamos los campos que deseamos incluir + fields = ['nombre', 'curp', 'email', 'password', 'confirmar_password'] + # Indicamos algún argumento extra necesario + extra_kwargs = { + 'password': {'write_only': True} + } + + # validamos que ambas contraseñas enviadas coincidan + def validate(self, data): + # realizamos la comparación de las contraseñas + if data['password'] != data['confirmar_password']: + # Si las contraseñas no coinciden se muestra un error + raise serializers.ValidationError("Atención: Las contraseñas no coinciden.") + return data + + # Función para crear el usuario, usando los datos ya validados + def create(self, validated_data): + # Creamos el usuario solicitante por medio de la funcion create_user del modelo + user = Usuario.objects.create_user( + email=validated_data['email'], + curp=validated_data['curp'], + nombre=validated_data['nombre'], + password=validated_data['password'], + is_admin=False, + is_staff=False + ) + # Retornamos el usuario creado + return user + diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index 93a2484..af668e1 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -16,7 +16,10 @@ Including another URLconf from . import views from django.urls import path from django.contrib.auth import views as auth_views +from .views import usuario_solicitante_create, lista_usuarios app_name = 'users' -urlpatterns = [ +urlpatterns = [ + path('nuevo-solicitante/', usuario_solicitante_create.as_view(), name = 'nuevo-solicitante'), + path('lista-usuarios/', lista_usuarios.as_view(), name = 'lista-usuarios'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index 91ea44a..c933837 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -1,3 +1,25 @@ +# Archivo con la funcionalidad necesaria para la gestión de usuarios en la API +# Autores: Adalberto Cerrillo Vázquez, +# Versión: 1.0 + from django.shortcuts import render +from rest_framework import generics +from rest_framework.permissions import AllowAny +from .models import Usuario +from .serializers import usuario_solicitante_serializer + + +# Funcionalidad para crear un usuario solicitante en el sistema +class usuario_solicitante_create(generics.CreateAPIView): + # obtenemos los usuarios del sistema + queryset = Usuario.objects.all() + # indicamos la clase de serializer a utilizar + serializer_class = usuario_solicitante_serializer + permission_classes = [AllowAny] -# Create your views here. +# Método de prueba para comprobar que las creaciones de usuario se realizen de manera adecuada +class lista_usuarios(generics.ListAPIView): + # obtenemos los usuarios del sistema + queryset = Usuario.objects.all() + # indicamos la clase de serializer a utilizar + serializer_class = usuario_solicitante_serializer \ No newline at end of file -- GitLab From e826466d1ea1f05340cad4592d11d19f8e24a8d3 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Thu, 27 Jun 2024 12:56:05 -0600 Subject: [PATCH 2/5] =?UTF-8?q?Usuario=20b=C3=A1sico,=20crear,=20ver=20y?= =?UTF-8?q?=20eliminar.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/cosiap_api/settings.py | 1 - cosiap_api/cosiap_api/urls.py | 2 +- cosiap_api/users/crear.py | 14 +++++ cosiap_api/users/serializers.py | 6 +- cosiap_api/users/tests.py | 6 +- cosiap_api/users/urls.py | 5 +- cosiap_api/users/views.py | 96 +++++++++++++++++++++++++------ 7 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 cosiap_api/users/crear.py diff --git a/cosiap_api/cosiap_api/settings.py b/cosiap_api/cosiap_api/settings.py index f08a3c9..3337084 100644 --- a/cosiap_api/cosiap_api/settings.py +++ b/cosiap_api/cosiap_api/settings.py @@ -44,7 +44,6 @@ INSTALLED_APPS = [ 'rest_framework_simplejwt', 'drf_spectacular', 'corsheaders', - 'users', ] diff --git a/cosiap_api/cosiap_api/urls.py b/cosiap_api/cosiap_api/urls.py index 01e0f52..e4f406e 100644 --- a/cosiap_api/cosiap_api/urls.py +++ b/cosiap_api/cosiap_api/urls.py @@ -24,7 +24,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, Spec urlpatterns = [ path('admin/', admin.site.urls), - path('usuarios/',include('users.urls')), + path('',include('users.urls')), path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), diff --git a/cosiap_api/users/crear.py b/cosiap_api/users/crear.py new file mode 100644 index 0000000..a1cc6bd --- /dev/null +++ b/cosiap_api/users/crear.py @@ -0,0 +1,14 @@ +import requests + +url = 'http://localhost:8000/crear-superusuario/' +data = { + "email": "adaladmin@gmail.com", + "curp": "ABC123456789XYZ01", + "nombre": "Adal Cerrillo", + "password": "password1234@" +} +headers = {'Content-Type': 'application/json'} + +response = requests.post(url, json=data, headers=headers) +print(response.status_code) +print(response.json()) diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py index 015e4c7..e2a7d6d 100644 --- a/cosiap_api/users/serializers.py +++ b/cosiap_api/users/serializers.py @@ -2,12 +2,11 @@ # Autores: Adalberto Cerrillo Vázquez, # Versión: 1.0 - from rest_framework import serializers from .models import Usuario # serializer para el usuario solicitante -class usuario_solicitante_serializer(serializers.ModelSerializer): +class usuario_serializer(serializers.ModelSerializer): # Creamos un campo de confirmación de la contraseña del solicitante confirmar_password = serializers.CharField(write_only=True) @@ -41,5 +40,4 @@ class usuario_solicitante_serializer(serializers.ModelSerializer): is_staff=False ) # Retornamos el usuario creado - return user - + return user \ No newline at end of file diff --git a/cosiap_api/users/tests.py b/cosiap_api/users/tests.py index 7ce503c..0266776 100644 --- a/cosiap_api/users/tests.py +++ b/cosiap_api/users/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase - -# Create your tests here. +# Archivo con las funcionalidades de prueba para la gestión de usuarios +# Autor: Adalberto Cerrillo Vázquez +# Versión: 1.0 \ No newline at end of file diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index af668e1..9aee9cd 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -16,10 +16,9 @@ Including another URLconf from . import views from django.urls import path from django.contrib.auth import views as auth_views -from .views import usuario_solicitante_create, lista_usuarios app_name = 'users' urlpatterns = [ - path('nuevo-solicitante/', usuario_solicitante_create.as_view(), name = 'nuevo-solicitante'), - path('lista-usuarios/', lista_usuarios.as_view(), name = 'lista-usuarios'), + path('usuarios/', views.usuario.as_view(), name = 'usuario-lista-crear'), + path('usuarios//', views.usuario.as_view(), name = 'ver-eliminar-usuario'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index c933837..a96497d 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -2,24 +2,86 @@ # Autores: Adalberto Cerrillo Vázquez, # Versión: 1.0 -from django.shortcuts import render -from rest_framework import generics -from rest_framework.permissions import AllowAny +from django.shortcuts import render, get_object_or_404 +from rest_framework import generics, status, permissions +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import AllowAny, IsAuthenticated from .models import Usuario -from .serializers import usuario_solicitante_serializer +from .serializers import usuario_serializer +from django.contrib.auth import authenticate + +# Funcionalidad para verificar que el usuario que realiza la eliminación sea un admin +class es_admin(permissions.BasePermission): + # método para determinar que un usuario tiene permisos + def has_permission( self, request, view): + return request.user and request.user.is_superuser # Funcionalidad para crear un usuario solicitante en el sistema -class usuario_solicitante_create(generics.CreateAPIView): - # obtenemos los usuarios del sistema - queryset = Usuario.objects.all() - # indicamos la clase de serializer a utilizar - serializer_class = usuario_solicitante_serializer - permission_classes = [AllowAny] - -# Método de prueba para comprobar que las creaciones de usuario se realizen de manera adecuada -class lista_usuarios(generics.ListAPIView): - # obtenemos los usuarios del sistema - queryset = Usuario.objects.all() - # indicamos la clase de serializer a utilizar - serializer_class = usuario_solicitante_serializer \ No newline at end of file +class usuario(APIView): + # permitimos que cualquier persona que haga una solicitud pueda crear un usuario + permission_classes_create = [AllowAny] + # permitimos solo a administradores eliminar un usuario + permission_classes_delete = [es_admin] + # permitimos solo administradores vean la lista de usuarios + permission_classes_list = [es_admin] + + + # función para verificar los permisos de la solicitud + def check_permissions( self, request ): + # verificamos si la solicitud fue hecha mediante delete + if request.method == 'DELETE': + # recorremos la lista de permisos de eliminación predefinada + for permission in self.permission_classes_delete: + # si el usuario no cuenta con permisos + if not permission().has_permission(request, self): + # denegamos los permisos para la eliminación + self.permission_denied( + request, + message=getattr(permission, 'message', None) + ) + + # Función para la obtención de la lista de usuarios + def get( self, request, *args, **kwargs ): + # si se quiere observar solo un usuario + if 'pk' in kwargs: + # obtenemos la instancia del usario + instance = get_object_or_404(Usuario, pk=kwargs['pk']) + # indicamos el serializer a utilizar y enviamos la instancia + serializer = usuario_serializer(instance) + # devolvemos los datos del usuario + return Response(serializer.data) + # si se desea ver la lista completa + else: + # indicamos el query set de todos los usuarios + queryset = Usuario.objects.all() + # indicamos el serializer a utilizar y enviamos el queryset + serializer = usuario_serializer(queryset, many=True) + # retornamos la lista de usuarios + return Response(serializer.data) + + # Función para la creación del usuario + def post( self, request, *args, **kwargs ): + # indicamos el serializer a utilizar + serializer = usuario_serializer(data=request.data) + # verificamos los datos enviados + if serializer.is_valid(): + # creamos la instancia del nuevo usuario + serializer.save() + # retornamos la respuesta de creación + return Response(serializer.data, status=status.HTTP_201_CREATED) + # en caso de que los datos sean incorrectos, retornamos un error. + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # Función para la eliminación del usuario + def delete( self, request, *args, **kwargs ): + # revisamos que el usuario que realizó el request tenga permisos + self.check_object_permissions(request, request.user) + # obtenemos la intancia del usuario a eliminar + instance = get_object_or_404(Usuario, pk=kwargs['pk']) + self.check_object_permissions(request, instance) + # eliminamos la intancia + instance.delete() + # devolvemos una respuesta sin contenido confirmando la eliminación + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file -- GitLab From ffb078d54fa441642e5d4b31c8611d25b762eff8 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Thu, 27 Jun 2024 15:32:09 -0600 Subject: [PATCH 3/5] =?UTF-8?q?verificaci=C3=B3n=20de=20email=20implementa?= =?UTF-8?q?do,=20validaci=C3=B3n=20de=20email=20repetido?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/cosiap_api/settings.py | 14 ++++ .../migrations/0002_campo_toke_usuario.py | 24 ++++++ cosiap_api/users/models.py | 57 +++++--------- cosiap_api/users/urls.py | 1 + cosiap_api/users/views.py | 76 +++++++++++++++++-- 5 files changed, 129 insertions(+), 43 deletions(-) create mode 100644 cosiap_api/users/migrations/0002_campo_toke_usuario.py diff --git a/cosiap_api/cosiap_api/settings.py b/cosiap_api/cosiap_api/settings.py index 3337084..face23a 100644 --- a/cosiap_api/cosiap_api/settings.py +++ b/cosiap_api/cosiap_api/settings.py @@ -14,6 +14,7 @@ import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_URL = 'http://localhost:8000/' # Cambiar esto por la URL base de la aplicación en producción # Quick-start development settings - unsuitable for production @@ -184,3 +185,16 @@ SPECTACULAR_SETTINGS = { #CORS_ALLOWED_ORIGINS = [ # "http://localhost:5173" #] + + +# Configuración de email +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = '' # NOTA ADAL: poner aquí un email que enviará los correos +EMAIL_HOST_PASSWORD = '' +""" +Para poner una contraseña del correo, necesitamos generar una contraseña de aplicación desde los +ajustes de seguridad de gmail. La nueva contraseña de aplicación debe llamarse Django App. +""" diff --git a/cosiap_api/users/migrations/0002_campo_toke_usuario.py b/cosiap_api/users/migrations/0002_campo_toke_usuario.py new file mode 100644 index 0000000..9882691 --- /dev/null +++ b/cosiap_api/users/migrations/0002_campo_toke_usuario.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.6 on 2024-06-27 20:34 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='usuario', + name='token_verificacion', + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + migrations.AlterField( + model_name='usuario', + name='is_active', + field=models.BooleanField(default=False), + ), + ] diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index 3d85f4e..d0c6b1e 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -3,49 +3,38 @@ from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.core.validators import RegexValidator from django.contrib.auth.models import PermissionsMixin from django.core.exceptions import ValidationError - -# Create your models here. - +import uuid class UsuarioManager(BaseUserManager): - def create_user(self, email, curp, nombre, password=None, is_admin=False, is_staff=False, is_active=True): + def create_user(self, email, curp, nombre, password=None, is_admin=False, is_staff=False, is_active=False): user = self.model( - email=self.normalize_email(email) + email=self.normalize_email(email), + curp=curp, + nombre=nombre, + is_staff=is_staff, + is_active=is_active ) - user.curp = curp - user.nombre = nombre user.set_password(password) - user.is_superuser = is_admin - user.is_staff = is_staff - user.is_active = is_active user.save(using=self._db) return user def create_superuser(self, email, curp, nombre, password=None, **extra_fields): user = self.model( - email=self.normalize_email(email) + email=self.normalize_email(email), + curp=curp, + nombre=nombre, + is_staff=True, + is_superuser=True, + is_active=True, + **extra_fields ) - user.curp = curp - user.nombre = nombre user.set_password(password) - user.is_superuser = True - user.is_staff = True - user.is_active = True user.save(using=self._db) return user - class Usuario(AbstractBaseUser, PermissionsMixin): - """ Clase bas de los usuarios del sistema. + """ Clase base de los usuarios del sistema. """ - Attributes: - nombre (str): Nombre del usuario. - curp (str): CURP. - email (str): Correo electronico. - is_staff (bool): Es parte del staff. - is_active (bool): Esta activo y puede ingresar al sistema. - """ - CURP_REGEX = r'^[A-Z]{1}[AEIOU]{1}[A-Z]{2}[0-9]{2}(0[1-9]|1[0-2])(0[1-9]|1[0-9]|2[0-9]|3[0-1])[HM]{1}(AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]{1}[0-9]{1}$' nombre = models.CharField( @@ -53,23 +42,19 @@ class Usuario(AbstractBaseUser, PermissionsMixin): curp = models.CharField( verbose_name="CURP", max_length=18, validators=[RegexValidator(CURP_REGEX,'Debe ser un CURP valido.')], - blank=False, null=False, unique=True) - email = models.EmailField(verbose_name="E-mail", blank=False, null=False, unique=True) + unique=True) + email = models.EmailField(verbose_name="E-mail", blank=False, null=False) is_staff = models.BooleanField(default=False) - is_active = models.BooleanField(default=True) - + is_active = models.BooleanField(default=False) # Cambiamos este campo a inactivo por defecto + token_verificacion = models.UUIDField(default=uuid.uuid4, editable=False) # agregamos un token de verificacion de email + EMAIL_FIELD = 'email' USERNAME_FIELD = 'curp' REQUIRED_FIELDS = ['nombre','email'] objects = UsuarioManager() def __str__(self): - str = "" - if self.is_superuser: - str = str + "SuperUser " - if self.is_staff: - str = str + "Staff " - return str + self.curp + return self.curp class Meta: ordering = ['-is_superuser', 'id' ] diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index 9aee9cd..4059971 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -21,4 +21,5 @@ app_name = 'users' urlpatterns = [ path('usuarios/', views.usuario.as_view(), name = 'usuario-lista-crear'), path('usuarios//', views.usuario.as_view(), name = 'ver-eliminar-usuario'), + path('usuarios/verificar//', views.verificar_token, name='verificar_token'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index a96497d..4fecda5 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -2,14 +2,18 @@ # Autores: Adalberto Cerrillo Vázquez, # Versión: 1.0 -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render, get_object_or_404, redirect from rest_framework import generics, status, permissions from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated from .models import Usuario +from django.contrib import messages from .serializers import usuario_serializer from django.contrib.auth import authenticate +from django.core.mail import send_mail +from django.conf import settings +import uuid # Funcionalidad para verificar que el usuario que realiza la eliminación sea un admin class es_admin(permissions.BasePermission): @@ -18,16 +22,15 @@ class es_admin(permissions.BasePermission): return request.user and request.user.is_superuser -# Funcionalidad para crear un usuario solicitante en el sistema +# Funcionalidad para crear un usuario en el sistema, ver sus datos o eliminarlo class usuario(APIView): # permitimos que cualquier persona que haga una solicitud pueda crear un usuario permission_classes_create = [AllowAny] # permitimos solo a administradores eliminar un usuario - permission_classes_delete = [es_admin] + permission_classes_delete = [AllowAny] # permitimos solo administradores vean la lista de usuarios permission_classes_list = [es_admin] - # función para verificar los permisos de la solicitud def check_permissions( self, request ): # verificamos si la solicitud fue hecha mediante delete @@ -67,13 +70,47 @@ class usuario(APIView): serializer = usuario_serializer(data=request.data) # verificamos los datos enviados if serializer.is_valid(): - # creamos la instancia del nuevo usuario - serializer.save() + # verificamos si la curp o el email no estan ya en uso por otro usuario activo + # obtebemos el email + email = serializer.validated_data.get('email') + # obtenemos la curp + curp = serializer.validated_data.get('curp') + # verificamos que el email no este en uso por otro usuario activo en el sistema + if Usuario.objects.filter(email=email).exists(): + # si el usuario ya existe, verificamos si es activo o no + usuario_existente = Usuario.objects.get(email=email) + if usuario_existente.is_active: + # Si es así, regresamos un error y lo indicamos con un mensaje + return Response({'error': 'Ya existe un usuario con el correo electrónico proporcionado.'}, status=status.HTTP_400_BAD_REQUEST) + else: + # en caso de que no sea activo, lo eliminamos para permitir que el registro del usuario continue + usuario_existente.delete() + # verificamos que la curp no este en uso por otro usuario + if Usuario.objects.filter(curp=curp).exists(): + # si el usuario existe, veirifcamos si es activo o no + usuario_existente = Usuario.objects.get(curp=curp) + if usuario_existente.is_active: + # Si es así, regresamos un error y lo indicamos con un mensaje + return Response({'error': 'Ya existe un usuario con la curp proporcionada.'}, status=status.HTTP_400_BAD_REQUEST) + else: + # si no es activo, lo eliminamos para permiteir que el registro del usuario continue + usuario_existente.delete() + # creamos la instancia del nuevo usuario como inactivo + usuario_nuevo = serializer.save(is_active = False) + # generamos y guardamos el token de verificacion + token = str(uuid.uuid4()) + # le asignamos el token al usuario + usuario_nuevo.token_verificacion = token + # guardamos cambios + usuario_nuevo.save() + # enviamos el correo de verificación + enviar_correo_verificacion(usuario_nuevo.email, token) # retornamos la respuesta de creación return Response(serializer.data, status=status.HTTP_201_CREATED) # en caso de que los datos sean incorrectos, retornamos un error. return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + # Función para la eliminación del usuario def delete( self, request, *args, **kwargs ): # revisamos que el usuario que realizó el request tenga permisos @@ -84,4 +121,29 @@ class usuario(APIView): # eliminamos la intancia instance.delete() # devolvemos una respuesta sin contenido confirmando la eliminación - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) + + +# Función para enviar el correo de confirmación +def enviar_correo_verificacion(email, token): + # tema o título del email + subject = 'Verificación de la cuenta' + # cuerpo del email + message = f'Para activar tu cuenta, haz clic en el siguiente enlace:\n\n{settings.BASE_URL}usuarios/verificar/{token}/' + # email remitente, configurado en settings + from_email = settings.EMAIL_HOST_USER + recipient_list = [email] + # enviamos el email con los datos configurados + send_mail(subject, message, from_email, recipient_list) + +# Función para verificar la cuenta del usuario +def verificar_token(request, token): + # obtenemos el usuario por medio del token de activación + usuario = get_object_or_404(Usuario, token_verificacion=token) + # Activar la cuenta del usuario + usuario.is_active = True + # guardamos los cambios en el usuario + usuario.save() + # Enviamos un mensaje de éxito + messages.success(request, 'Tu cuenta ha sido verificada correctamente. Ahora puedes iniciar sesión.') + return redirect('users:usuario-lista-crear') # NOTA ADAL: Este redirect lo cambiaremos en su momento por la url al que sea necesario mandar al usuario \ No newline at end of file -- GitLab From 596a6c5c5674cd57b815f5a966065e8a8879b0fa Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Fri, 28 Jun 2024 15:35:24 -0600 Subject: [PATCH 4/5] validacion curp y email repetidos corregida, verificacion de email sin uso de base de datos, creacion modelo solicitante --- cosiap_api/cosiap_api/settings.py | 12 -- cosiap_api/cosiap_api/urls.py | 4 +- cosiap_api/users/admin.py | 7 +- .../0003_eliminacion_campo_token_usuario.py | 17 +++ .../0004_creacion_modelo_solicitante.py | 34 +++++ cosiap_api/users/models.py | 30 +++- cosiap_api/users/tokens.py | 10 ++ cosiap_api/users/urls.py | 2 +- cosiap_api/users/views.py | 128 ++++++++++-------- 9 files changed, 172 insertions(+), 72 deletions(-) create mode 100644 cosiap_api/users/migrations/0003_eliminacion_campo_token_usuario.py create mode 100644 cosiap_api/users/migrations/0004_creacion_modelo_solicitante.py create mode 100644 cosiap_api/users/tokens.py diff --git a/cosiap_api/cosiap_api/settings.py b/cosiap_api/cosiap_api/settings.py index face23a..ea7b0b0 100644 --- a/cosiap_api/cosiap_api/settings.py +++ b/cosiap_api/cosiap_api/settings.py @@ -186,15 +186,3 @@ SPECTACULAR_SETTINGS = { # "http://localhost:5173" #] - -# Configuración de email -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' -EMAIL_PORT = 587 -EMAIL_USE_TLS = True -EMAIL_HOST_USER = '' # NOTA ADAL: poner aquí un email que enviará los correos -EMAIL_HOST_PASSWORD = '' -""" -Para poner una contraseña del correo, necesitamos generar una contraseña de aplicación desde los -ajustes de seguridad de gmail. La nueva contraseña de aplicación debe llamarse Django App. -""" diff --git a/cosiap_api/cosiap_api/urls.py b/cosiap_api/cosiap_api/urls.py index e4f406e..2ac902b 100644 --- a/cosiap_api/cosiap_api/urls.py +++ b/cosiap_api/cosiap_api/urls.py @@ -15,6 +15,8 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin +from django.conf import settings +from django.conf.urls.static import static from django.urls import path, include from rest_framework_simplejwt.views import ( TokenObtainPairView, @@ -32,4 +34,4 @@ urlpatterns = [ # API Doc UI: path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), -] \ No newline at end of file +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/cosiap_api/users/admin.py b/cosiap_api/users/admin.py index 2d41865..d74e966 100644 --- a/cosiap_api/users/admin.py +++ b/cosiap_api/users/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from users.models import Usuario +from users.models import Usuario, Solicitante # Register your models here. @@ -29,5 +29,6 @@ class UserAdmin(UserAdmin): ordering = ['id'] list_filter = ('is_staff', 'is_superuser', 'is_active') - -admin.site.register(Usuario, UserAdmin) \ No newline at end of file + +admin.site.register(Usuario, UserAdmin) +admin.site.register(Solicitante) \ No newline at end of file diff --git a/cosiap_api/users/migrations/0003_eliminacion_campo_token_usuario.py b/cosiap_api/users/migrations/0003_eliminacion_campo_token_usuario.py new file mode 100644 index 0000000..fa82d61 --- /dev/null +++ b/cosiap_api/users/migrations/0003_eliminacion_campo_token_usuario.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.6 on 2024-06-28 18:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_campo_toke_usuario'), + ] + + operations = [ + migrations.RemoveField( + model_name='usuario', + name='token_verificacion', + ), + ] diff --git a/cosiap_api/users/migrations/0004_creacion_modelo_solicitante.py b/cosiap_api/users/migrations/0004_creacion_modelo_solicitante.py new file mode 100644 index 0000000..4080e62 --- /dev/null +++ b/cosiap_api/users/migrations/0004_creacion_modelo_solicitante.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.6 on 2024-06-28 20:07 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_eliminacion_campo_token_usuario'), + ] + + operations = [ + migrations.CreateModel( + name='Solicitante', + fields=[ + ('usuario_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('ap_paterno', models.CharField(max_length=50, verbose_name='Apellido Paterno')), + ('ap_materno', models.CharField(blank=True, max_length=50, null=True, verbose_name='Apellido Materno')), + ('telefono', models.CharField(max_length=10, verbose_name='Teléfono')), + ('RFC', models.CharField(max_length=13, unique=True, validators=[django.core.validators.RegexValidator('^[A-Z&Ñ]{4}\\d{6}[A-Z0-9]{3}$', 'Debe ser un RFC válido.')], verbose_name='RFC')), + ('direccion', models.CharField(max_length=255, verbose_name='Dirección')), + ('codigo_postal', models.CharField(max_length=5, verbose_name='Código Postal')), + ('poblacion', models.CharField(max_length=255, verbose_name='Población')), + ('INE', models.FileField(blank=True, null=True, upload_to='ine_files/', verbose_name='INE')), + ], + options={ + 'abstract': False, + }, + bases=('users.usuario',), + ), + ] diff --git a/cosiap_api/users/models.py b/cosiap_api/users/models.py index d0c6b1e..d90e7c6 100644 --- a/cosiap_api/users/models.py +++ b/cosiap_api/users/models.py @@ -43,10 +43,9 @@ class Usuario(AbstractBaseUser, PermissionsMixin): verbose_name="CURP", max_length=18, validators=[RegexValidator(CURP_REGEX,'Debe ser un CURP valido.')], unique=True) - email = models.EmailField(verbose_name="E-mail", blank=False, null=False) + email = models.EmailField(verbose_name="E-mail", blank=False, null=False, unique=True) is_staff = models.BooleanField(default=False) is_active = models.BooleanField(default=False) # Cambiamos este campo a inactivo por defecto - token_verificacion = models.UUIDField(default=uuid.uuid4, editable=False) # agregamos un token de verificacion de email EMAIL_FIELD = 'email' USERNAME_FIELD = 'curp' @@ -58,3 +57,30 @@ class Usuario(AbstractBaseUser, PermissionsMixin): class Meta: ordering = ['-is_superuser', 'id' ] + + +# clase Solicitante que hereda de Usuario +class Solicitante(Usuario): + # validador para el formato del RFC + RFC_REGEX = r'^[A-Z&Ñ]{4}\d{6}[A-Z0-9]{3}$' + + # campos para los apellidos + ap_paterno = models.CharField(verbose_name='Apellido Paterno',max_length=50) + # campo de apellido materno es opcional + ap_materno = models.CharField(verbose_name='Apellido Materno', max_length=50, null=True, blank=True) + # campo para telefono, longitud maxima de 10 + telefono = models.CharField(verbose_name='Teléfono', max_length=10) + # campo para el RFC, longitud de 13 + RFC = models.CharField(verbose_name='RFC', max_length=13, validators=[RegexValidator(RFC_REGEX,'Debe ser un RFC válido.')], unique=True) + # campo para la dirección + direccion = models.CharField(verbose_name='Dirección', max_length=255) + # campo para el código postal, longitud 5 + codigo_postal = models.CharField(verbose_name='Código Postal', max_length=5) + # campo para la poblacion + poblacion = models.CharField(verbose_name='Población', max_length=255) + # campo para la identificación oficial + INE = models.FileField(verbose_name='INE', upload_to='ine_files/', null=True, blank=True) + + + + diff --git a/cosiap_api/users/tokens.py b/cosiap_api/users/tokens.py new file mode 100644 index 0000000..661419f --- /dev/null +++ b/cosiap_api/users/tokens.py @@ -0,0 +1,10 @@ +from django.contrib.auth.tokens import PasswordResetTokenGenerator +import six + +class AccountActivationTokenGenerator(PasswordResetTokenGenerator): + def _make_hash_value(self, user, timestamp): + return ( + six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active) + ) + +account_activation_token = AccountActivationTokenGenerator() \ No newline at end of file diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index 4059971..64b4312 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -21,5 +21,5 @@ app_name = 'users' urlpatterns = [ path('usuarios/', views.usuario.as_view(), name = 'usuario-lista-crear'), path('usuarios//', views.usuario.as_view(), name = 'ver-eliminar-usuario'), - path('usuarios/verificar//', views.verificar_token, name='verificar_token'), + path('usuarios/verificar///', views.verificar_token, name='verificar_token'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index 4fecda5..cbac2c3 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -13,7 +13,10 @@ from .serializers import usuario_serializer from django.contrib.auth import authenticate from django.core.mail import send_mail from django.conf import settings -import uuid +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes, force_str +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from .tokens import account_activation_token # Funcionalidad para verificar que el usuario que realiza la eliminación sea un admin class es_admin(permissions.BasePermission): @@ -27,7 +30,7 @@ class usuario(APIView): # permitimos que cualquier persona que haga una solicitud pueda crear un usuario permission_classes_create = [AllowAny] # permitimos solo a administradores eliminar un usuario - permission_classes_delete = [AllowAny] + permission_classes_delete = [es_admin] # permitimos solo administradores vean la lista de usuarios permission_classes_list = [es_admin] @@ -44,13 +47,25 @@ class usuario(APIView): request, message=getattr(permission, 'message', None) ) + elif request.method == 'GET': + # recorremos la lista de permisos de visualización predefinada + for permission in self.permission_classes_list: + # si el usuario no cuenta con permisos + if not permission().has_permission(request, self): + # denegamos los permisos para la eliminación + self.permission_denied( + request, + message=getattr(permission, 'message', None) + ) # Función para la obtención de la lista de usuarios def get( self, request, *args, **kwargs ): # si se quiere observar solo un usuario if 'pk' in kwargs: + self.check_object_permissions(request, request.user) # obtenemos la instancia del usario instance = get_object_or_404(Usuario, pk=kwargs['pk']) + self.check_object_permissions(request, instance) # indicamos el serializer a utilizar y enviamos la instancia serializer = usuario_serializer(instance) # devolvemos los datos del usuario @@ -66,45 +81,36 @@ class usuario(APIView): # Función para la creación del usuario def post( self, request, *args, **kwargs ): - # indicamos el serializer a utilizar - serializer = usuario_serializer(data=request.data) + # obtenemos el email y la curp directamente del request + email = request.data.get('email') + curp = request.data.get('curp') + # filtramos en la base de datos para obtener al usuario con la misma curp + curp_exist = Usuario.objects.filter(curp=curp, is_active=False) + # filtramos en la base de datos para obtener al usuario con el mismo email + email_exist = Usuario.objects.filter(email=email, is_active=False).exclude(curp=curp) + # si la curp existe + if curp_exist: + # extraemos el usuario asociado a ella + curp_exist = Usuario.objects.get(curp=curp) + else: + # en caso de que no exista dejamos curp_exist como None para que el serializer inicie una instancia vacia + curp_exist = None + # si el email existe + if email_exist: + # eliminamos el usuario asociado a el + email_exist = Usuario.objects.get(email=email).delete() + # sobreescribimos el serializer enviando la instancia encontrada + serializer = usuario_serializer(data=request.data, instance=curp_exist) # verificamos los datos enviados if serializer.is_valid(): - # verificamos si la curp o el email no estan ya en uso por otro usuario activo - # obtebemos el email - email = serializer.validated_data.get('email') - # obtenemos la curp - curp = serializer.validated_data.get('curp') - # verificamos que el email no este en uso por otro usuario activo en el sistema - if Usuario.objects.filter(email=email).exists(): - # si el usuario ya existe, verificamos si es activo o no - usuario_existente = Usuario.objects.get(email=email) - if usuario_existente.is_active: - # Si es así, regresamos un error y lo indicamos con un mensaje - return Response({'error': 'Ya existe un usuario con el correo electrónico proporcionado.'}, status=status.HTTP_400_BAD_REQUEST) - else: - # en caso de que no sea activo, lo eliminamos para permitir que el registro del usuario continue - usuario_existente.delete() - # verificamos que la curp no este en uso por otro usuario - if Usuario.objects.filter(curp=curp).exists(): - # si el usuario existe, veirifcamos si es activo o no - usuario_existente = Usuario.objects.get(curp=curp) - if usuario_existente.is_active: - # Si es así, regresamos un error y lo indicamos con un mensaje - return Response({'error': 'Ya existe un usuario con la curp proporcionada.'}, status=status.HTTP_400_BAD_REQUEST) - else: - # si no es activo, lo eliminamos para permiteir que el registro del usuario continue - usuario_existente.delete() # creamos la instancia del nuevo usuario como inactivo usuario_nuevo = serializer.save(is_active = False) - # generamos y guardamos el token de verificacion - token = str(uuid.uuid4()) - # le asignamos el token al usuario - usuario_nuevo.token_verificacion = token - # guardamos cambios - usuario_nuevo.save() - # enviamos el correo de verificación - enviar_correo_verificacion(usuario_nuevo.email, token) + # extraemos el id del usuario y lo decodificacmos a base64 + uid = urlsafe_base64_encode(force_bytes(usuario_nuevo.pk)) + # creamos el token asociado al usuario + token = account_activation_token.make_token(usuario_nuevo) + # enviamos el correo de verificación con el token + enviar_correo_verificacion(usuario_nuevo.email, uid, token) # retornamos la respuesta de creación return Response(serializer.data, status=status.HTTP_201_CREATED) # en caso de que los datos sean incorrectos, retornamos un error. @@ -125,25 +131,41 @@ class usuario(APIView): # Función para enviar el correo de confirmación -def enviar_correo_verificacion(email, token): - # tema o título del email +def enviar_correo_verificacion(email, uid, token): + # tema o título del correo subject = 'Verificación de la cuenta' - # cuerpo del email - message = f'Para activar tu cuenta, haz clic en el siguiente enlace:\n\n{settings.BASE_URL}usuarios/verificar/{token}/' - # email remitente, configurado en settings + # cuerpo del correo con el enlace de verificación, que incluye el token y el id del usuario + message = f'Para activar tu cuenta, haz clic en el siguiente enlace:\n\n{settings.BASE_URL}usuarios/verificar/{uid}/{token}/' + # extraemos el correo remitente desde la configuración del settings from_email = settings.EMAIL_HOST_USER + # indicamos el correo o correos destinatarios recipient_list = [email] - # enviamos el email con los datos configurados + # enviamos el email de verificación send_mail(subject, message, from_email, recipient_list) # Función para verificar la cuenta del usuario -def verificar_token(request, token): - # obtenemos el usuario por medio del token de activación - usuario = get_object_or_404(Usuario, token_verificacion=token) - # Activar la cuenta del usuario - usuario.is_active = True - # guardamos los cambios en el usuario - usuario.save() - # Enviamos un mensaje de éxito - messages.success(request, 'Tu cuenta ha sido verificada correctamente. Ahora puedes iniciar sesión.') - return redirect('users:usuario-lista-crear') # NOTA ADAL: Este redirect lo cambiaremos en su momento por la url al que sea necesario mandar al usuario \ No newline at end of file +def verificar_token(request, uidb64, token): + # Si el usuario asociado al token existe + try: + # extraemos el id del usuario presente en el token + uid = force_str(urlsafe_base64_decode(uidb64)) + # extraemos el usuario de la base de datos + usuario = Usuario.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, Usuario.DoesNotExist): + # si el usuario no existe lo seteamos a None + usuario = None + # Utilizamos la función check_token() para verificar que el token sea correcto y pertenezca al usuario + if usuario is not None and account_activation_token.check_token(usuario, token): + # activamos la cuenta del usuario + usuario.is_active = True + # guardamos los cambios + usuario.save() + # si es necesario, enviamos un mensaje de exito + messages.success(request, 'Tu cuenta ha sido verificada correctamente. Ahora puedes iniciar sesión.') + # NOTA ADAL: Esta redirección se cambiara según las necesidades futuras + return redirect('users:usuario-lista-crear') + else: + # si algo sale mal, indicamos simplemente que el token no es válido + messages.error(request, 'El enlace de verificación no es válido o ha expirado.') + # NOTA ADAL: Esta redirección se cambiara según las necesidades futuras + return redirect('users:usuario-lista-crear') \ No newline at end of file -- GitLab From 5fb3917835f7cdc8d7d8c5e82bc2b6c0d047377f Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Fri, 28 Jun 2024 15:43:33 -0600 Subject: [PATCH 5/5] Archivo no deseado eliminado --- cosiap_api/users/crear.py | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 cosiap_api/users/crear.py diff --git a/cosiap_api/users/crear.py b/cosiap_api/users/crear.py deleted file mode 100644 index a1cc6bd..0000000 --- a/cosiap_api/users/crear.py +++ /dev/null @@ -1,14 +0,0 @@ -import requests - -url = 'http://localhost:8000/crear-superusuario/' -data = { - "email": "adaladmin@gmail.com", - "curp": "ABC123456789XYZ01", - "nombre": "Adal Cerrillo", - "password": "password1234@" -} -headers = {'Content-Type': 'application/json'} - -response = requests.post(url, json=data, headers=headers) -print(response.status_code) -print(response.json()) -- GitLab