Mudanza de repo

This commit is contained in:
2025-09-22 18:43:29 -06:00
parent 26fe36ca52
commit d11d543bdc
193 changed files with 10998 additions and 0 deletions

0
api/cuser/__init__.py Normal file
View File

51
api/cuser/admin.py Normal file
View File

@@ -0,0 +1,51 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ('username', 'email', 'first_name', 'last_name', 'organizacion', 'profile_picture', 'password1', 'password2')
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ('username', 'email', 'first_name', 'last_name', 'organizacion', 'profile_picture')
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'is_importador', 'organizacion')
list_filter = ('is_staff', 'is_active', 'organizacion')
search_fields = ('username', 'email', 'first_name', 'last_name')
ordering = ('username',)
# Fieldsets para editar un usuario
fieldsets = (
(None, {'fields': ('username', 'password')}),
('Información personal', {'fields': ('first_name', 'last_name', 'email', 'organizacion', 'profile_picture', 'is_importador', 'rfc')}),
('Permisos', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Fechas importantes', {'fields': ('last_login', 'date_joined')}),
)
# Fieldsets para crear un nuevo usuario
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'username', 'email', 'first_name', 'last_name',
'organizacion', 'profile_picture',
'password1', 'password2',
'is_staff', 'is_active', 'is_importador'
)
}),
)
admin.site.register(CustomUser, CustomUserAdmin)

6
api/cuser/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CuserConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.cuser'

View File

@@ -0,0 +1,45 @@
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework.response import Response
from rest_framework import status
from django.conf import settings
class CookieTokenObtainPairView(TokenObtainPairView):
"""
Custom view to set JWT tokens as HttpOnly cookies.
"""
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
access = response.data.get('access')
refresh = response.data.get('refresh')
# Remove tokens from body (optional, for extra security)
response.data.pop('access', None)
response.data.pop('refresh', None)
# Set cookies
cookie_settings = {
'httponly': True,
'secure': True, # Set to True if using HTTPS
'samesite': 'Lax',
'path': '/'
}
response.set_cookie('access_token', access, max_age=60*5, **cookie_settings) # 5 min
response.set_cookie('refresh_token', refresh, max_age=60*60*24*7, **cookie_settings) # 7 days
return response
class CookieTokenRefreshView(TokenRefreshView):
"""
Custom view to refresh JWT tokens and set as HttpOnly cookies.
"""
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
access = response.data.get('access')
response.data.pop('access', None)
cookie_settings = {
'httponly': True,
'secure': True, # Set to True if using HTTPS
'samesite': 'Lax',
'path': '/'
}
response.set_cookie('access_token', access, max_age=60*5, **cookie_settings) # 5 min
return response

View File

@@ -0,0 +1,51 @@
# Generated by Django 5.2.3 on 2025-07-14 16:14
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('organization', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile_pictures/')),
('is_importador', models.BooleanField(default=False, help_text='Indicates if the user is an importer')),
('rfc', models.CharField(blank=True, help_text='RFC of the user', max_length=13, null=True, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='users', to='organization.organizacion')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'Custom User',
'verbose_name_plural': 'Custom Users',
'ordering': ['username'],
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.3 on 2025-07-30 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cuser', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='rfc',
field=models.CharField(blank=True, help_text='RFC of the user', max_length=13, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.3 on 2025-07-31 16:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cuser', '0002_alter_customuser_rfc'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='rfc',
field=models.CharField(blank=True, help_text='RFC of the user', max_length=1, null=True),
),
]

View File

23
api/cuser/models.py Normal file
View File

@@ -0,0 +1,23 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class CustomUser(AbstractUser):
"""
Custom user model that extends the default Django user model.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, null=True, blank=True, related_name='users')
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
is_importador = models.BooleanField(default=False, help_text="Indicates if the user is an importer")
rfc = models.CharField(max_length=1, null=True, blank=True, help_text="RFC of the user")
def __str__(self):
return self.username
class Meta:
verbose_name = 'Custom User'
verbose_name_plural = 'Custom Users'
ordering = ['username']

View File

@@ -0,0 +1,24 @@
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
from django.urls import reverse
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.contrib.auth.tokens import default_token_generator
from django.template.loader import render_to_string
def send_password_reset_email(user, request):
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
# Enlace directo al frontend
reset_link = f"https://efc-aduanasoft.com/user/password-reset-confirm/{uid}/{token}/"
subject = 'Recuperación de contraseña'
context = {
'username': user.username,
'reset_link': reset_link,
}
text_content = f'Hola {user.username},\nPara restablecer tu contraseña haz clic en el siguiente enlace: {reset_link}'
html_content = render_to_string('email/password_reset_email.html', context)
email = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [user.email])
email.attach_alternative(html_content, "text/html")
email.send(fail_silently=False)

29
api/cuser/serializers.py Normal file
View File

@@ -0,0 +1,29 @@
from rest_framework import serializers
from .models import CustomUser
from django.contrib.auth.models import Group
class CustomUserSerializer(serializers.ModelSerializer):
"""
Serializer for the CustomUser model.
"""
password = serializers.CharField(write_only=True)
groups = serializers.PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
rfc = serializers.CharField(max_length=20, required=False, allow_blank=True)
class Meta:
model = CustomUser
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'password', 'profile_picture', 'organizacion', 'is_importador', 'rfc', 'is_active', 'is_superuser', 'groups']
read_only_fields = ['id', 'organizacion', 'is_superuser']
def create(self, validated_data):
groups = validated_data.pop('groups', [])
password = validated_data.pop('password')
user = CustomUser(**validated_data)
user.set_password(password)
user.save()
if groups:
user.groups.set(groups)
return user

91
api/cuser/tests.py Normal file
View File

@@ -0,0 +1,91 @@
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from api.organization.models import Organizacion
from .models import CustomUser
User = get_user_model()
class CustomUserViewSetTests(APITestCase):
def setUp(self):
self.org = Organizacion.objects.create(nombre="OrgTest", is_active=True, is_verified=True)
self.org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
self.admin = User.objects.create_user(username="admin", password="adminpass", organizacion=self.org)
self.admin.groups.create(name="admin")
self.superuser = User.objects.create_superuser(username="superuser", password="superpass")
self.importador = User.objects.create_user(username="importador", password="importpass", organizacion=self.org2, is_importador=True, rfc="RFC123456789")
self.importador.groups.create(name="importador")
self.user = User.objects.create_user(username="user1", password="userpass", organizacion=self.org)
self.client = APIClient()
def test_admin_sees_only_own_org_users(self):
user2 = User.objects.create_user(username="user2", password="userpass2", organizacion=self.org2)
self.client.force_authenticate(user=self.admin)
url = reverse('customuser-list')
response = self.client.get(url)
usernames = [u['username'] for u in response.data]
self.assertIn("admin", usernames)
self.assertIn("user1", usernames)
self.assertNotIn("user2", usernames)
def test_superuser_sees_all_users(self):
user2 = User.objects.create_user(username="user2", password="userpass2", organizacion=self.org2)
self.client.force_authenticate(user=self.superuser)
url = reverse('customuser-list')
response = self.client.get(url)
usernames = [u['username'] for u in response.data]
self.assertIn("admin", usernames)
self.assertIn("user1", usernames)
self.assertIn("user2", usernames)
def test_importador_cannot_create_user(self):
self.client.force_authenticate(user=self.importador)
url = reverse('customuser-list')
data = {
"username": "newuser",
"email": "newuser@example.com",
"first_name": "New",
"last_name": "User",
"password": "newpass123"
}
response = self.client.post(url, data)
self.assertNotIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_list_users(self):
url = reverse('customuser-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(len(response.data) >= 1)
def test_me_endpoint(self):
url = reverse('customuser-me')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['username'], self.admin.username)
def test_create_user_as_admin(self):
url = reverse('customuser-list')
data = {
"username": "newuser",
"email": "newuser@example.com",
"first_name": "New",
"last_name": "User",
"password": "newpass123"
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['username'], "newuser")
def test_update_user_as_admin(self):
url = reverse('customuser-detail', args=[str(self.user.id)])
data = {"first_name": "Updated"}
response = self.client.patch(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['first_name'], "Updated")
def test_profile_picture_view(self):
# No profile picture, should return 404
url = reverse('profile-picture', args=[str(self.user.id)])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

18
api/cuser/urls.py Normal file
View File

@@ -0,0 +1,18 @@
from django.urls import path, include
from .views import ProfilePictureView, ActivateUserView, PasswordResetRequestView, PasswordResetConfirmView, CustomUserViewSet
customuser_me = CustomUserViewSet.as_view({'get': 'me'})
from rest_framework.routers import DefaultRouter
from .views import CustomUserViewSet
router = DefaultRouter()
router.register(r'users', CustomUserViewSet, basename='customuser')
urlpatterns = [
path('', include(router.urls)),
path('me/', customuser_me, name='user-me'),
path('profile-picture/<uuid:user_id>/', ProfilePictureView.as_view(), name='profile-picture'),
path('activate/<uidb64>/<token>/', ActivateUserView.as_view(), name='activate'),
path('password-reset/', PasswordResetRequestView.as_view(), name='password_reset'),
path('password-reset-confirm/<uidb64>/<token>/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
]

28
api/cuser/utils.py Normal file
View File

@@ -0,0 +1,28 @@
import uuid
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.contrib.auth.tokens import default_token_generator
def send_activation_email(user, request):
"""
Envía un correo de activación al usuario con un link único.
"""
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
activation_link = request.build_absolute_uri(
reverse('cuser:activate', kwargs={'uidb64': uid, 'token': token})
)
subject = 'Activa tu cuenta'
context = {
'username': user.username,
'activation_link': activation_link,
}
text_content = f'Hola {user.username},\nPor favor haz clic en el siguiente enlace para activar tu cuenta: {activation_link}'
html_content = render_to_string('email/activation_email.html', context)
email = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [user.email])
email.attach_alternative(html_content, "text/html")
email.send(fail_silently=False)

275
api/cuser/views.py Normal file
View File

@@ -0,0 +1,275 @@
from .password_reset_utils import send_password_reset_email
from django.contrib.auth import get_user_model
from django.utils.encoding import force_str
# Vista para solicitar recuperación de contraseña
from rest_framework import status
import uuid
from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404, redirect
from rest_framework.views import APIView
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from rest_framework.exceptions import PermissionDenied
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
from .serializers import CustomUserSerializer
from .models import CustomUser
from api.logger.mixins import LoggingMixin
from api.vucem.models import Vucem
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
from .utils import send_activation_email
from django.utils.http import urlsafe_base64_decode
from django.contrib.auth.tokens import default_token_generator
from rest_framework.views import APIView
from django.utils.encoding import force_str
from django.conf import settings
class CustomPagination(PageNumberPagination):
"""
Paginación personalizada con parámetros flexibles
- Si no se especifica page_size, devuelve todos los resultados (sin paginación)
- Si se especifica page_size, usa paginación normal
"""
page_size = None # Sin paginación por defecto
page_size_query_param = 'page_size'
max_page_size = 1000 # Límite máximo de seguridad
page_query_param = 'page'
def paginate_queryset(self, queryset, request, view=None):
"""
Si no se especifica page_size en los parámetros, devolver None (sin paginación)
Si se especifica, usar paginación normal
"""
# Verificar si se especificó page_size en la query
if self.page_size_query_param not in request.query_params:
# No hay page_size, devolver None para indicar "sin paginación"
return None
# Hay page_size, usar paginación normal
try:
page_size = int(request.query_params[self.page_size_query_param])
if page_size <= 0:
return None
# Establecer el page_size temporalmente para esta request
self.page_size = min(page_size, self.max_page_size)
except (ValueError, TypeError):
return None
return super().paginate_queryset(queryset, request, view)
class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for CustomUser model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSameOrganization )]
pagination_class = CustomPagination
model = CustomUser
serializer_class = CustomUserSerializer
filterset_fields = ['username', 'email', 'first_name', 'last_name', 'organizacion', 'is_importador']
my_tags = ['User Profile']
def get_permissions(self):
# Permitir eliminar usuarios solo a admin, Agente Aduanal y user de la misma organización
if self.action == 'destroy':
user = self.request.user
if not (
user.is_superuser or
user.groups.filter(name='admin').exists() or
user.groups.filter(name='Agente Aduanal').exists() or
user.groups.filter(name='user').exists()
):
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo admin, Agente Aduanal o user pueden eliminar usuarios.")
elif self.action in ['create', 'update', 'partial_update']:
if not (self.request.user.is_superuser or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='Importador').exists()) :
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo admin o superusuario pueden modificar usuarios.")
return super().get_permissions()
def perform_destroy(self, instance):
# Solo permitir eliminar usuarios de la misma organización
if self.request.user.is_superuser or instance.organizacion == self.request.user.organizacion:
instance.delete()
else:
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo puedes eliminar usuarios de tu organización.")
def get_queryset(self):
# Si es importador, solo puede ver su propio usuario
if self.request.user.groups.filter(name='importador').exists() or self.request.user.groups.filter(name='Importador').exists():
return CustomUser.objects.filter(pk=self.request.user.pk)
# Otros roles: filtrar por organización
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
# Always assign the creator's organization
if self.request.user.groups.filter(name='admin').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
if not self.request.user.organizacion:
raise PermissionDenied("Los administradores deben tener una organización asignada para crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
if self.request.user.is_superuser:
# If superuser, allow creating users without organization
user = serializer.save(is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
if self.request.user.groups.filter(name='developer').exists():
# Developers can create users but must assign an organization
if not self.request.user.organizacion:
raise PermissionDenied("Los desarrolladores deben tener una organización asignada para crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
if self.request.user.groups.filter(name='importador').exists():
# No puedes crear un usuario si eres importador
raise PermissionDenied("Los importadores no pueden crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def me(self, request):
"""
Endpoint para obtener la información del usuario autenticado.
GET /api/v1/user/me/
"""
serializer = self.get_serializer(request.user)
return Response(serializer.data)
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def change_password(self, request, pk=None):
"""
Endpoint para cambiar la contraseña de un usuario.
Solo el propio usuario o un admin/superuser pueden cambiarla.
POST /user/users/{id}/change_password/
Body: {"old_password": "actual", "new_password": "nueva"}
"""
user = self.get_object()
current_user = request.user
# Solo el propio usuario, admin o superuser pueden cambiar la contraseña
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists() or user == current_user):
raise PermissionDenied("No tienes permiso para cambiar la contraseña de este usuario.")
old_password = request.data.get('old_password')
new_password = request.data.get('new_password')
if not new_password:
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
# Si no es admin/superuser, debe validar old_password
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists()):
if not old_password or not user.check_password(old_password):
return Response({'detail': 'La contraseña actual es incorrecta.'}, status=400)
user.set_password(new_password)
user.save()
return Response({'detail': 'Contraseña cambiada correctamente.'}, status=200)
class ActivateUserView(APIView):
"""
Vista para activar usuario desde el link enviado por correo.
"""
permission_classes = [] # Permitir acceso público a la activación de usuario
my_tags = ['User Authentication']
def get(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
from .models import CustomUser
user = CustomUser.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
user = None
if user is not None and default_token_generator.check_token(user, token):
user.is_active = True
user.save()
# Aquí puedes redirigir a una página de éxito o login
return redirect(settings.SITE_URL + 'login?activated=1')
else:
return Response({'detail': 'El enlace de activación no es válido o ha expirado.'}, status=400)
def perform_update(self, serializer):
# Only allow update if user is in the same organization
instance = self.get_object()
if instance.organizacion != self.request.user.organizacion:
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo puedes actualizar usuarios de tu organización.")
password = serializer.validated_data.pop('password', None)
user = serializer.save()
if password:
user.set_password(password)
user.save()
class ProfilePictureView(LoggingMixin, APIView):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
my_tags = ['User Profile']
def get(self, request, user_id):
# Obtiene el usuario (automáticamente 404 si no existe)
user = get_object_or_404(CustomUser, pk=user_id)
# El permiso IsOwnerOrAdmin ya verificó que request.user == user o es admin
# Así que no necesitas validar manualmente los permisos aquí.
if not user.profile_picture:
raise Http404("El usuario no tiene imagen de perfil")
return FileResponse(user.profile_picture.open('rb'))
class PasswordResetRequestView(APIView):
permission_classes = [] # Permitir acceso público a la recuperación de contraseña
my_tags = ['User Authentication']
def post(self, request):
email = request.data.get('email')
username = request.data.get('username')
if not email or not username:
return Response({'detail': 'Se requieren username y email.'}, status=400)
User = get_user_model()
try:
user = User.objects.get(email=email, username=username)
except User.DoesNotExist:
return Response({'detail': 'No existe usuario con ese username y email.'}, status=404)
send_password_reset_email(user, request) # Usa template HTML
return Response({'detail': 'Se ha enviado un correo para restablecer la contraseña.'}, status=status.HTTP_200_OK)
# Vista para confirmar recuperación de contraseña
class PasswordResetConfirmView(APIView):
permission_classes = [] # Permitir acceso público a la confirmación de recuperación de contraseña
my_tags = ['User Authentication']
def post(self, request, uidb64, token):
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_decode
User = get_user_model()
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return Response({'detail': 'Enlace inválido.'}, status=400)
if not default_token_generator.check_token(user, token):
return Response({'detail': 'Token inválido o expirado.'}, status=400)
password = request.data.get('password')
if not password:
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
user.set_password(password)
user.save()
return Response({'detail': 'Contraseña restablecida correctamente.'}, status=status.HTTP_200_OK)