From bc6e112c2c5bb1ec3a16441207a21b00def17b65 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Fri, 5 Jul 2024 13:12:40 -0600 Subject: [PATCH 1/3] =?UTF-8?q?Edici=C3=B3n=20de=20usuario=20por=20ID,=20r?= =?UTF-8?q?establecer=20contrase=C3=B1a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/users/tokens.py | 3 +- cosiap_api/users/urls.py | 4 +- cosiap_api/users/views.py | 94 +++++++++++++++++++++++++++++++++++--- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/cosiap_api/users/tokens.py b/cosiap_api/users/tokens.py index 661419f..810462b 100644 --- a/cosiap_api/users/tokens.py +++ b/cosiap_api/users/tokens.py @@ -7,4 +7,5 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator): 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 +account_activation_token = AccountActivationTokenGenerator() +token_generator = PasswordResetTokenGenerator() \ No newline at end of file diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index f3e1c61..a499120 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -28,5 +28,7 @@ urlpatterns = [ path('/', views.usuario.as_view(), name = 'ver-eliminar-usuario'), path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-list'), path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-detail'), - path('verificar-correo///', views.verificar_token, name='verificar_token'), + path('verificar-correo///', views.verificar_token, name='verificar-token'), + path('restablecer-password/', views.reset_password.as_view(), name='reset-password'), + path('nueva-password///', views.nueva_password.as_view(), name='nueva-password'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index 7c2a150..27b8012 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -16,7 +16,8 @@ from django.conf import settings 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 +from django.contrib.auth.hashers import make_password +from .tokens import account_activation_token, token_generator from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from rest_framework.response import Response from rest_framework import status @@ -39,9 +40,6 @@ class CustomTokenObtainPairView(TokenObtainPairView): ) # Eliminar el refresh token de la respuesta JSON del response.data['refresh'] - # Agregar mensaje de éxito - response.data['message'] = {'succes':'Login exitoso'} - return response class CustomTokenRefreshView(TokenRefreshView): @@ -101,6 +99,8 @@ class BasePermissionAPIView(APIView): if not permission().has_permission(request, self): # negamos el acceso a la view self.permission_denied(request, message=getattr(permission, 'message', None)) + + # Funcionalidad para crear un usuario en el sistema, ver sus datos o eliminarlo class usuario(BasePermissionAPIView): @@ -236,8 +236,18 @@ class solicitante(BasePermissionAPIView): # Solicitud put para actualizar los datos del solicitante def put(self, request, *args, **kwargs): - # recuperamos el solicitante de la base de datos, enviando un error si no se encuentra - solicitante = get_object_or_404(Solicitante, id=request.user.id) + # si se envía un ID, es porque un administrador quiere editar al usuario vía ID + if 'pk' in kwargs: + # verificamos que solamente los administradores puedan reaizar ediciones por ID + if request.user.is_staff: + # extraemos al solicitante de la base de datos si este ultimo existe + solicitante = get_object_or_404(Solicitante, pk=kwargs['pk']) + else: + # si el usuario no es admin, no esta autorizado para la edición por ID + return Response({"detail": "Usted no tiene permisos para realizar esta acción."}, status=status.HTTP_401_UNAUTHORIZED) + else: + # recuperamos el solicitante de la base de datos, enviando un error si no se encuentra + solicitante = get_object_or_404(Solicitante, id=request.user.id) # inicializamos el serializer con los datos precargados serializer = solicitante_serializer(instance=solicitante, data=request.data) # si el serilizer es valido @@ -250,6 +260,78 @@ class solicitante(BasePermissionAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +# Clase para realizar el restablecimiento de una contraseña olvidada +class reset_password(APIView): + # esta view la puede ejecutar cualquiera + permission_classes = [AllowAny] + + # Solicitudes de tipo post en donde se enviará el email. + def post(self, request, *args, **kwargs): + # extraemos el email de la solicitud + email = request.data.get('email') + # intentamos extraer el usuario al que le pertenece el email + try: + # extraemos al ususario por medio del email + usuario = Usuario.objects.get(email=email) + # creamos un token para el restablecimiento de la contraseña + token = token_generator.make_token(usuario) + # obtenemos el user id y lo codificamos a base64 + uid = urlsafe_base64_encode(force_bytes(usuario.pk)) + # enviamos el correo de restablecimiento + enviar_correo_reset_password(email, uid, token) + # confirmamos el envio del correo + return Response({"detail": "Correo de restablecimiento enviado."}, status=status.HTTP_200_OK) + # si el usuario no existe + except Usuario.DoesNotExist: + # enviamos un mensaje para indicar que el correo no esta registrado + return Response({"detail": "El correo electrónico no está registrado."}, status=status.HTTP_400_BAD_REQUEST) + + +# Clase para ingresar la nueva password una vez el usuario confirmó la acción vía email +class nueva_password(APIView): + permission_classes = [AllowAny] + + # método post donde se enviará las nueva password + def post(self, request, uidb64, token): + # intentamos extraer los datos del usuario + try: + # decodificamos el id recibido + uid = urlsafe_base64_decode(uidb64).decode() + # extraemos al usuario de la base de datos + usuario = Usuario.objects.get(pk=uid) + # en caso de que el usuario no se encuentre + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + # se setea a None + usuario = None + # verificamos que el usuario exista y que el token enviado pertenezca a el + if usuario is not None and token_generator.check_token(usuario, token): + # extraemos la nueva password de la solicitud + password = request.data.get('password') + # asignamos la nueva contraseña al usuario + usuario.password = make_password(password) + # guardamos los cambios realizados + usuario.save() + # si todo salio bien, enviamos la confirmación + return Response({"detail": "La contraseña ha sido restablecida exitosamente."}, status=status.HTTP_200_OK) + else: + # si algo salió mal, indicamos que el enlace es inválido + return Response({"detail": "El enlace de restablecimiento es inválido."}, status=status.HTTP_400_BAD_REQUEST) + + +# método para enviar el correo de reestablecimiento de contraseña +def enviar_correo_reset_password(email, uid, token): + # título del correo + subject = 'Restablecer contraseña' + # Cuerpo del correo electrónico + message = f'Para restablecer tu contraseña, haz click en confirmar:\n\n{settings.BASE_URL}api/usuarios/nueva-password/{uid}/{token}/' + # correo remitente + from_email = settings.EMAIL_HOST_USER + # indicamos el correo o correos destinatarios + recipient_list = [email] + # enviamos el email de restablecimiento de contraseña + send_mail(subject, message, from_email, recipient_list) + # Función para enviar el correo de confirmación def enviar_correo_verificacion(email, uid, token): # tema o título del correo -- GitLab From 02e1226f641c6cb07eeb95e70d1a31b22a00c9a8 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Fri, 5 Jul 2024 14:52:59 -0600 Subject: [PATCH 2/3] Solicitante puede editar su perfil por ID --- cosiap_api/users/views.py | 43 ++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index 27b8012..ab5daf2 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -40,6 +40,8 @@ class CustomTokenObtainPairView(TokenObtainPairView): ) # Eliminar el refresh token de la respuesta JSON del response.data['refresh'] + # Agregar mensaje de éxito + response.data['message'] = {'succes':'Login exitoso'} return response class CustomTokenRefreshView(TokenRefreshView): @@ -160,7 +162,8 @@ class usuario(BasePermissionAPIView): # 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) + response_data = {'data': serializer.data, 'message': {'success': 'Creación del usuario exitosa.'}} + return Response(response_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) @@ -175,7 +178,8 @@ class usuario(BasePermissionAPIView): # eliminamos la intancia instance.delete() # devolvemos una respuesta sin contenido confirmando la eliminación - return Response(status=status.HTTP_204_NO_CONTENT) + response_data = {'message': {'success': 'Eliminación exitosa.'}} + return Response(response_data, status=status.HTTP_204_NO_CONTENT) # Clase para manejar la lógica del solicitante (Creación, edición) @@ -228,9 +232,9 @@ class solicitante(BasePermissionAPIView): # Revisamos que los datos estén completos (debido a que son opcionales) if solicitante.datos_completos: # Permitimos el acceso - return Response({"detail": "Acesso permitido."}, status=status.HTTP_200_OK) + return Response({"message": {'success': 'Acceso permitido.'}}, status=status.HTTP_200_OK) # Si no están completos, se solicita que se completen los datos - return Response({"detail": "Favor de completar sus datos."}, status=status.HTTP_200_OK) + return Response({"message": {'error': 'Favor de completar sus datos.'}}, status=status.HTTP_200_OK) # Si hay errores en los datos, enviamos un bad request return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -238,13 +242,11 @@ class solicitante(BasePermissionAPIView): def put(self, request, *args, **kwargs): # si se envía un ID, es porque un administrador quiere editar al usuario vía ID if 'pk' in kwargs: - # verificamos que solamente los administradores puedan reaizar ediciones por ID - if request.user.is_staff: - # extraemos al solicitante de la base de datos si este ultimo existe - solicitante = get_object_or_404(Solicitante, pk=kwargs['pk']) - else: - # si el usuario no es admin, no esta autorizado para la edición por ID - return Response({"detail": "Usted no tiene permisos para realizar esta acción."}, status=status.HTTP_401_UNAUTHORIZED) + # extraemos al solicitante por el ID + solicitante = get_object_or_404(Solicitante, pk=kwargs['pk']) + if not (request.user.is_staff or (solicitante.id == request.user.id)): + # si el usuario no es admin o el id enviado no pertenece al solicitante, no esta autorizado para la edición por ID + return Response({"message": {'error': 'Usted no tiene permisos para realizar esta acción.'}}, status=status.HTTP_401_UNAUTHORIZED) else: # recuperamos el solicitante de la base de datos, enviando un error si no se encuentra solicitante = get_object_or_404(Solicitante, id=request.user.id) @@ -255,7 +257,7 @@ class solicitante(BasePermissionAPIView): # guardamos los cambios serializer.save() # enviamos el mensaje de la actualización de los datos - return Response({"detail": "Datos actualizados exitosamente."}, status=status.HTTP_200_OK) + return Response({"message": {'success': 'Datos actualizados exitosamente.'}}, status=status.HTTP_200_OK) # si hay errores en los datos, regresamos un bad request return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -281,17 +283,16 @@ class reset_password(APIView): # enviamos el correo de restablecimiento enviar_correo_reset_password(email, uid, token) # confirmamos el envio del correo - return Response({"detail": "Correo de restablecimiento enviado."}, status=status.HTTP_200_OK) + return Response({"message": {'success': 'Correo de restablecimiento enviado.'}}, status=status.HTTP_200_OK) # si el usuario no existe except Usuario.DoesNotExist: # enviamos un mensaje para indicar que el correo no esta registrado - return Response({"detail": "El correo electrónico no está registrado."}, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": {'error': 'El correo no está registrado.'}}, status=status.HTTP_400_BAD_REQUEST) # Clase para ingresar la nueva password una vez el usuario confirmó la acción vía email class nueva_password(APIView): permission_classes = [AllowAny] - # método post donde se enviará las nueva password def post(self, request, uidb64, token): # intentamos extraer los datos del usuario @@ -313,10 +314,10 @@ class nueva_password(APIView): # guardamos los cambios realizados usuario.save() # si todo salio bien, enviamos la confirmación - return Response({"detail": "La contraseña ha sido restablecida exitosamente."}, status=status.HTTP_200_OK) + return Response({"message": {'success': 'La contraseña ha sido restablecida exitosamente.'}}, status=status.HTTP_200_OK) else: # si algo salió mal, indicamos que el enlace es inválido - return Response({"detail": "El enlace de restablecimiento es inválido."}, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": {'error': 'El enlace no es válido.'}}, status=status.HTTP_400_BAD_REQUEST) # método para enviar el correo de reestablecimiento de contraseña @@ -363,11 +364,7 @@ def verificar_token(request, uidb64, token): # 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') + return Response({"message": {'success': 'Cuenta verificada exitosamente.'}}, status=status.HTTP_200_OK) 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 + return Response({"message": {'error': 'El token de verificación es inválido o ha expirado.'}}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file -- GitLab From ea4222795b5bd9c7717161162068c143263cf7a9 Mon Sep 17 00:00:00 2001 From: AdalbertoCV <34152734@uaz.edu.mx> Date: Mon, 8 Jul 2024 13:22:44 -0600 Subject: [PATCH 3/3] =?UTF-8?q?Crear=20cuenta=20de=20administrador=20y=20a?= =?UTF-8?q?daptaci=C3=B3n=20de=20verificaci=C3=B3n=20de=20email=20a=20APIV?= =?UTF-8?q?iew?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cosiap_api/users/admin_views.py | 64 +++++++++++++++++++++++++++++++++ cosiap_api/users/serializers.py | 34 ++++++++++++++++++ cosiap_api/users/urls.py | 8 +++-- cosiap_api/users/views.py | 47 ++++++++++++------------ 4 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 cosiap_api/users/admin_views.py diff --git a/cosiap_api/users/admin_views.py b/cosiap_api/users/admin_views.py new file mode 100644 index 0000000..60d9c97 --- /dev/null +++ b/cosiap_api/users/admin_views.py @@ -0,0 +1,64 @@ +# Archivo con la funcionalidad necesaria para la gestión de administradores en la API +# Autores: Adalberto Cerrillo Vázquez, Rafael Uribe Caldera +# Versión: 1.0 +from rest_framework.views import APIView +from .serializers import admin_serializer +from .permisos import es_admin +from .models import Usuario +from .views import enviar_correo_verificacion +from rest_framework import status +from rest_framework.response import Response +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from .tokens import account_activation_token +from django.utils.encoding import force_bytes + +# clase de APIView con el método post para la creación de el administrador +class administrador(APIView): + # permitimos que unicamente un administrador pueda crear otras cuentas de admin + permission_classes = [es_admin] + + # método get para obtener la lista de administradores + def get(self, request, *args, **kwargs): + # indicamos el query set de todos los usuarios + queryset = Usuario.objects.filter(is_staff=True) + # indicamos el serializer a utilizar y enviamos el queryset + serializer = admin_serializer(queryset, many=True) + # retornamos la lista de usuarios + return Response(serializer.data) + + # Método post para realziar el registro del nuevo admin + def post(self, request, *args, **kwargs): + # obtenemos el email del request + email = request.data.get("email") + # verificamos que el email no esté repetido + try: + email_exist = Usuario.objects.get(email=email) + except Usuario.DoesNotExist: + email_exist = None + # si no hay datos duplicado procedemos con el registro del admin + if email_exist is None: + # inicializamos el serializer de admin + serializer = admin_serializer(data=request.data) + # verificamos que los datos sean válidos + if serializer.is_valid(): + # creamos la instancia del nuevo usuario como inactivo + usuario_nuevo = serializer.save(is_active = False) + # incluimos la contraseña del usuario + usuario_nuevo.set_password(request.data.get('password')) + # 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 + response_data = {'data': serializer.data, 'message': {'success': 'Creación del administrador exitosa.'}} + return Response(response_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) + else: + # en caso de que algún dato este repetido lo indicamos por un mensaje + response_data = {'message': {'error': 'Este email ya esta en uso por otro usuario.'}} + return Response(response_data, status = status.HTTP_400_BAD_REQUEST) + + diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py index 4b47cac..aa289e7 100644 --- a/cosiap_api/users/serializers.py +++ b/cosiap_api/users/serializers.py @@ -5,6 +5,40 @@ from rest_framework import serializers from .models import Usuario, Solicitante, DatosBancarios + +# serializar para el administrador +class admin_serializer(serializers.ModelSerializer): + # campo para la confirmación del password + confirmar_password = serializers.CharField(write_only=True) + + class Meta: + # Indicamos que se usará el modelo de Usuario + model = Usuario + # Indicamos que campos se le mostrarán en el formulario + fields = ['nombre', 'curp', 'email', 'password', 'confirmar_password'] + # Indicamos el password como write only (para que no sea legible) y el nombre sea nulleable + extra_kwargs = {'password': {'write_only': True}, 'nombre': {'required': False}} + + # 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 + + # funcion para la creacion del admin + def create(self, validated_data): + # creamos al administrador usando el metodo create_superuser() de la clase Usuario + user = Usuario.objects.create_superuser( + email = validated_data['email'], + curp= validated_data['curp'], + nombre= validated_data['nombre'], + password=validated_data['password'] + ) + # retornamos el usuario creado + return user + # serializer para el usuario solicitante class usuario_serializer(serializers.ModelSerializer): # Creamos un campo de confirmación de la contraseña del solicitante diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index a499120..146fa7f 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -17,6 +17,7 @@ from . import views from django.urls import path from django.contrib.auth import views as auth_views from .views import CustomTokenObtainPairView, CustomTokenRefreshView +from .admin_views import administrador app_name = 'users' @@ -26,9 +27,10 @@ urlpatterns = [ path('', views.usuario.as_view(), name = 'usuario-lista-crear'), path('/', views.usuario.as_view(), name = 'ver-eliminar-usuario'), - path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-list'), - path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-detail'), - path('verificar-correo///', views.verificar_token, name='verificar-token'), + path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-lista-crear'), + path('solicitantes/', views.solicitante.as_view(), name = 'solicitante-detalle'), + path('verificar-correo///', views.verificar_correo.as_view(), name='verificar-correo'), path('restablecer-password/', views.reset_password.as_view(), name='reset-password'), path('nueva-password///', views.nueva_password.as_view(), name='nueva-password'), + path('administradores/', administrador.as_view() , name = 'administrador-lista-crear'), ] \ No newline at end of file diff --git a/cosiap_api/users/views.py b/cosiap_api/users/views.py index ab5daf2..4078c35 100644 --- a/cosiap_api/users/views.py +++ b/cosiap_api/users/views.py @@ -346,25 +346,28 @@ def enviar_correo_verificacion(email, uid, token): # 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, 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 - return Response({"message": {'success': 'Cuenta verificada exitosamente.'}}, status=status.HTTP_200_OK) - else: - # si algo sale mal, indicamos simplemente que el token no es válido - return Response({"message": {'error': 'El token de verificación es inválido o ha expirado.'}}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file +# clase para manejar la verificación del correo electrónico +class verificar_correo(APIView): + permission_classes = [AllowAny] + + def get(self, 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 + return Response({"message": {'success': 'Cuenta verificada exitosamente.'}}, status=status.HTTP_200_OK) + else: + # si algo sale mal, indicamos simplemente que el token no es válido + return Response({"message": {'error': 'El token de verificación es inválido o ha expirado.'}}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file -- GitLab