diff --git a/project/api/middlewares.py b/project/api/middlewares.py index ca7f6d5..e3ad3a8 100755 --- a/project/api/middlewares.py +++ b/project/api/middlewares.py @@ -2,11 +2,10 @@ from django.urls import resolve from django.http import HttpResponse from .models import Usuario, Persona from decouple import config +from project.settings import SECRET_KEY import jwt import logging -private_key = config('SECRET_JWT') - class ApiMiddleware: def __init__(self, get_response): self.get_response = get_response @@ -16,11 +15,6 @@ class ApiMiddleware: if request.path[0:5] != '/api/': response = self.get_response(request) return response - - # se omite esta regla en login - if request.path == '/api/auth/' and request.method == 'POST': - response = self.get_response(request) - return response match = resolve(request.path) logging.error(match) @@ -30,6 +24,22 @@ class ApiMiddleware: response = self.get_response(request) return response + if match.url_name == 'auth_login' and request.method == 'POST': + response = self.get_response(request) + return response + + if match.url_name == 'auth_recuperar': + response = self.get_response(request) + return response + + if match.url_name == 'auth_info': + response = self.get_response(request) + return response + + if match.url_name == 'auth_contrasena': + response = self.get_response(request) + return response + # se omite esta regla al mostrar informacion publica de paradero if match.url_name == 'paradero-info-public' and request.method == 'GET': response = self.get_response(request) @@ -42,7 +52,7 @@ class ApiMiddleware: token = authorization[1] try: - decoded = jwt.decode(token, private_key, algorithms=["HS256"]) + decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) except jwt.ExpiredSignatureError: return HttpResponse('token ya no es valido', status = 400) except jwt.InvalidTokenError: diff --git a/project/api/models.py b/project/api/models.py index dcbbe10..f8debe0 100755 --- a/project/api/models.py +++ b/project/api/models.py @@ -369,24 +369,13 @@ class Usuario(models.Model): vigente = models.BooleanField(blank=True, null=True) superuser = models.BooleanField(blank=True, null=True) id_rol = models.ForeignKey(Rol, models.DO_NOTHING, db_column='id_rol', blank=False, null=False) + clave = models.CharField(max_length=100, blank=True, null=True) class Meta: managed = False db_table = 'usuario' -class UsuarioClave(models.Model): - login = models.OneToOneField(Usuario, models.DO_NOTHING, db_column='login', primary_key=True) - clave = models.CharField(max_length=60, blank=True, null=True) - clave_anterior = models.CharField(max_length=60, blank=True, null=True) - fecha_modificacion = models.DateField(blank=True, null=True) - codigo = models.DecimalField(max_digits=8, decimal_places=0, blank=True, null=True) - - class Meta: - managed = False - db_table = 'usuario_clave' - - class Vehiculo(models.Model): ppu = models.CharField(primary_key=True, max_length=10) id_tipo_vehiculo = models.ForeignKey(TipoVehiculo, models.DO_NOTHING, db_column='id_tipo_vehiculo', blank=True, null=True) diff --git a/project/api/templates/correo_recuperar.html b/project/api/templates/correo_recuperar.html new file mode 100644 index 0000000..bb65dfc --- /dev/null +++ b/project/api/templates/correo_recuperar.html @@ -0,0 +1,23 @@ + + + + + + Document + + +
+

Estimado/a {{ nombre }}

+

Para crear una nueva contraseña, es necesario que ingrese el siguiente código: + {{ codigo }} +

+ +

Pinche el siguiente botón para crear una nueva contraseña.

+

+ + CREAR NUEVA CONTRASEÑA + +

+
+ + \ No newline at end of file diff --git a/project/api/urls.py b/project/api/urls.py index a4d0aa1..6df33dc 100755 --- a/project/api/urls.py +++ b/project/api/urls.py @@ -32,7 +32,10 @@ router.register('roles-lineas', rol_linea.RolLineaViewSet, basename='rol_linea') urlpatterns = [ path('', include(router.urls)), - path('auth/', auth.jwt_login, name='auth'), + path('auth/', auth.jwt_login, name='auth_login'), + path('auth/recuperar/', auth.recuperar, name='auth_recuperar'), + path('auth/info/', auth.info_token, name='auth_info'), + path('auth/nueva-contrasena/', auth.nueva_contrasena, name='auth_contrasena'), path('mapas/paraderos/', mapa.paraderos, name='mapa-paraderos'), path('mapas/rutas/', mapa.rutas, name='mapa-rutas'), path('upload/zip/', upload.upload_zip, name='upload_zip'), diff --git a/project/api/views/auth.py b/project/api/views/auth.py index 3b48d95..89f93eb 100755 --- a/project/api/views/auth.py +++ b/project/api/views/auth.py @@ -1,18 +1,22 @@ from django.views.decorators.csrf import csrf_exempt -from django.http import HttpResponse -from django.http import JsonResponse +from django.contrib.auth.hashers import check_password, make_password +from django.http import HttpResponse, JsonResponse from rest_framework.decorators import action, api_view, schema +from project.settings import SECRET_KEY, EMAIL_HOST -from .. import models, schemas +from api import models, schemas +from datetime import datetime, timedelta from decouple import config import json import jwt -from datetime import datetime, timedelta import logging +import random -private_key = config('SECRET_JWT') +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import get_template # Views jwt @csrf_exempt @@ -21,26 +25,27 @@ private_key = config('SECRET_JWT') @schema(schemas.AuthSchema()) def jwt_login(request): if request.method == 'POST': - count = models.Usuario.objects.filter(vigente = True).count() - logging.error(f'count usuario vigente = {count}') - - # validar username y password + # validar rut y password input = json.loads(request.body) - username = input['username'] - password = input['password'] + rut = input['rut'].replace('.','').replace('-','') + + if rut != '0': + dv = rut[-1].upper() + rut = rut[:-1] + usuario = None - if count > 0: - usuario = models.Usuario.objects.filter(login = username, vigente = True).values().first() - elif username == '0' and password == '0': + if rut == '0' and password == '0': usuario = { 'login': '0', 'clave': '0' } + + # solo se permite usuario 0 si no existen usuarios vigentes + count = models.Usuario.objects.filter(vigente = True).count() + if count > 0: + return HttpResponse('Acceso no valido', status=400) + else: + usuario = models.Usuario.objects.filter(vigente=1, rut__rut=rut, rut__dv=dv).values().first() - if not usuario: - return HttpResponse('Acceso no valido', status=400) - - if username != '0': - clave = models.UsuarioClave.objects.filter(login = username).first() - if not clave or clave.clave != password: + if not check_password(input['password'], usuario['clave']): return HttpResponse('Acceso no valido', status=400) ahora = datetime.utcnow() @@ -52,7 +57,132 @@ def jwt_login(request): 'exp': manana, # ahora + timedelta(minutes=60), 'login': usuario['login'] } - token = jwt.encode(payload, private_key, algorithm="HS256") + token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") return JsonResponse({ 'token': token }) + elif request.method == 'GET': - return JsonResponse(request.jwt_info) \ No newline at end of file + return JsonResponse(request.jwt_info) + + + +@csrf_exempt +@action(detail=False, methods=['post']) +@api_view(['POST']) +def recuperar(request): + input = json.loads(request.body) + rut = input['rut'].replace('.','').replace('-','') + + dv = rut[-1].upper() + rut = rut[:-1] + + persona = models.Persona.objects.filter(rut=rut, dv=dv).first() + usuario = models.Usuario.objects.filter(rut=rut, vigente=True).first() + + if usuario == None or persona == None: + return HttpResponse('Acceso no valido', status=400) + + if persona.email != input['email'].lower(): + return HttpResponse('Acceso no valido', status=400) + + codigo_aleatorio = random.randint(100000, 999999) + ahora = datetime.utcnow() + expira = ahora + timedelta(minutes=5) + payload = { + 'iat': ahora, + 'exp': expira, + 'rut': f'{persona.rut}', + 'codigo': codigo_aleatorio + } + token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") + vinculo = f"{http_referer(request)}/?s={token}#/new-password" + + exito = enviar_correo(persona.email, 'Recuperar acceso', { + 'nombre': f'{persona.nombres} {persona.apellido_a} {persona.apellido_b}', + 'codigo': codigo_aleatorio, + 'vinculo': vinculo + }) + + return JsonResponse({ 'ok': exito, 'HTTP_REFERER': request.META['HTTP_REFERER'] }) + + + +@csrf_exempt +@action(detail=False, methods=['post']) +@api_view(['POST']) +def info_token(request): + input = json.loads(request.body) + token = input['token'] + try: + decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + persona = models.Persona.objects.filter(rut=decoded['rut']).first() + + return JsonResponse({ + 'nombres': persona.nombres, + 'apellido_a': persona.apellido_a, + 'apellido_b': persona.apellido_b + }) + except jwt.ExpiredSignatureError: + return HttpResponse('token ya no es valido', status = 400) + except jwt.InvalidTokenError: + return HttpResponse('token es invalido', status = 400) + + + +@csrf_exempt +@action(detail=False, methods=['post']) +@api_view(['POST']) +def nueva_contrasena(request): + input = json.loads(request.body) + token = input['token'] + try: + decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + + if f"{decoded['codigo']}" != input['codigo']: + return HttpResponse('código es invalido', status = 400) + + usuario = models.Usuario.objects.filter(rut = decoded['rut']).first() + if usuario == None: + return HttpResponse('Usuario no encontrado', status = 400) + + usuario.clave = make_password(input['password']) + usuario.save() + + return JsonResponse({ 'ok': True }) + + except jwt.ExpiredSignatureError: + return HttpResponse('token ya no es valido', status = 400) + except jwt.InvalidTokenError: + return HttpResponse('token es invalido', status = 400) + except Exception as e: + logging.error(e) + return HttpResponse('error al cambiar contraseña', status = 500) + + + + +def enviar_correo(destinatario, asunto, contenido): + try: + template = get_template('correo_recuperar.html') # Ruta al template del correo + contenido_renderizado = template.render(contenido) + + mensaje = EmailMultiAlternatives(asunto, '', settings.EMAIL_HOST_USER, [destinatario]) + mensaje.attach_alternative(contenido_renderizado, 'text/html') + mensaje.send() + return True + except Exception as e: + print(f'EMAIL_HOST: {EMAIL_HOST}', flush=True) + print(f'ERROR: {e}', flush=True) + return False + + + + +def http_referer(request): + if 'HTTP_REFERER' in request.META: + referer = request.META['HTTP_REFERER'] + else: + protocol = request.scheme + host = request.META['HTTP_HOST'] + port = request.META['SERVER_PORT'] + referer = f'{protocol}://{host}' + return referer \ No newline at end of file diff --git a/project/api/views/paradero.py b/project/api/views/paradero.py index bb16613..35e4f30 100755 --- a/project/api/views/paradero.py +++ b/project/api/views/paradero.py @@ -28,7 +28,7 @@ class ParaderoViewSet(viewsets.ModelViewSet): @action(detail=False, methods=['get'], url_path='info-public/(?P\S+)') def info_public(self, request, pk=None): - if hasattr(request.META,'HTTP_REFERER'): + if 'HTTP_REFERER' in request.META: referer = request.META['HTTP_REFERER'] else: protocol = request.scheme diff --git a/project/api/views/usuario.py b/project/api/views/usuario.py index 62438d6..1cfedd7 100755 --- a/project/api/views/usuario.py +++ b/project/api/views/usuario.py @@ -1,6 +1,7 @@ from django.db import transaction from django.http import HttpResponse, JsonResponse +from django.contrib.auth.hashers import make_password from rest_framework import viewsets from rest_framework.response import Response from rest_framework.decorators import action @@ -60,24 +61,10 @@ class UsuarioViewSet(viewsets.ModelViewSet): ) usuario.save() - # logging.error(f'clave = {input["clave"]}') if input['clave']: logging.error('Modificar clave de usuario') - clave = models.UsuarioClave.objects.filter(login = usuario.login).first() - if clave: - logging.error('Clave Usuario ya existe') - clave.clave_anterior = clave.clave - clave.clave = input['clave'] - clave.fecha_modificacion = datetime.datetime.now() - clave.save() - else: - logging.error('Clave Usuario se creará') - clave = models.UsuarioClave( - login = usuario, - clave = input['clave'], - fecha_modificacion = datetime.datetime.now() - ) - clave.save() + usuario.clave = make_password(input['clave']) + usuario.save() return Response({ 'rut': persona.rut, @@ -101,13 +88,11 @@ class UsuarioViewSet(viewsets.ModelViewSet): def update(self, request, *args, **kwargs): input = json.loads(request.body) - logging.error(input) try: pk = input['rut'] with transaction.atomic(): # validaciones se realiza a nivel del model - persona = models.Persona.objects.filter(rut = pk).first() rol = models.Rol.objects.filter(id_rol = input.get('id_rol')).first() @@ -119,23 +104,8 @@ class UsuarioViewSet(viewsets.ModelViewSet): if 'clave' in input: logging.error('Modificar clave de usuario') - logging.error(f'clave = {input["clave"]}') - - clave = models.UsuarioClave.objects.filter(login = usuario.login).first() - if clave: - logging.error('Clave Usuario ya existe') - clave.clave_anterior = clave.clave - clave.clave = input['clave'] - clave.fecha_modificacion = datetime.datetime.now() - clave.save() - else: - logging.error('Clave Usuario se creará') - clave = models.UsuarioClave( - login = usuario, - clave = input['clave'], - fecha_modificacion = datetime.datetime.now() - ) - clave.save() + usuario.clave = make_password(input['clave']) + usuario.save() return Response({ 'rut': persona.rut, diff --git a/project/project/settings.py b/project/project/settings.py index 6ecb485..138674e 100644 --- a/project/project/settings.py +++ b/project/project/settings.py @@ -9,9 +9,12 @@ https://docs.djangoproject.com/en/4.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ - +from dotenv import load_dotenv from pathlib import Path from decouple import config +import os + +load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -62,7 +65,10 @@ ROOT_URLCONF = 'project.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ BASE_DIR / 'dist' ], + 'DIRS': [ + os.path.join(BASE_DIR, 'dist'), + os.path.join(BASE_DIR, 'api', 'templates') + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -169,4 +175,11 @@ LOGGING = { }, }, } -""" \ No newline at end of file +""" + + +EMAIL_HOST = config('SMTP_HOST') +EMAIL_PORT = config('SMTP_PORT', 587) +EMAIL_HOST_USER = config('SMTP_USER', 'tu_correo@gmail.com') # Tu dirección de correo +EMAIL_HOST_PASSWORD = config('SMTP_PASS', 'tu_contraseña') # Tu contraseña de correo +EMAIL_USE_TLS = config('SMTP_PROTOCOL') == 'tls' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5511fdb..b268bca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ djangorestframework django-cors-headers django-filter coreapi +python-dotenv python-decouple PyJWT pymongo