diff --git a/cosiap_api/users/admin_views.py b/cosiap_api/users/admin_views.py new file mode 100644 index 0000000000000000000000000000000000000000..60d9c975064acd9d7d5a9ecd9f8b41f288f50490 --- /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 4b47cac256528f316c349b741df08cc138cfc57f..aa289e7f467019da67ebab48b1902a48a9a5a31d 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/tokens.py b/cosiap_api/users/tokens.py index 661419fb0825f70deeed52f3524793c5fde15b11..810462b7a00633931c95226c7d8e3c5e73c1170e 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 d73a4e649fa2f8ae67542de654b21a31cce98b46..5ecef7317888e693d385e8c377d049b6c1f11fae 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -2,6 +2,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' @@ -11,7 +12,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 d5b7da6e8ddf0ff56fc9ff683a4ea8cbd4c14646..27a7d06d4be3907854f223a76cdf194e6d058fba 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 @@ -101,6 +102,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): @@ -160,7 +163,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 +179,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,16 +233,24 @@ 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) # 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: + # 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) # inicializamos el serializer con los datos precargados serializer = solicitante_serializer(instance=solicitante, data=request.data) # si el serilizer es valido @@ -245,11 +258,82 @@ 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) + +# 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({"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({"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 + 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({"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({"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 +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 @@ -263,29 +347,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 - 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 +# 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