| """ |
| auth_api/views.py — Admin authentication views. |
| |
| Endpoints: |
| POST /api/auth/login/ — authenticate admin, set HttpOnly cookies |
| POST /api/auth/logout/ — clear cookies |
| POST /api/auth/refresh/ — refresh access token via cookie |
| GET /api/auth/me/ — return current user info (requires valid access cookie) |
| """ |
|
|
| from datetime import datetime, timezone as dt_timezone |
|
|
| from django.conf import settings |
| from rest_framework import status |
| from rest_framework.decorators import api_view, permission_classes, throttle_classes |
| from rest_framework.permissions import AllowAny, IsAuthenticated |
| from rest_framework.response import Response |
| from rest_framework.throttling import ScopedRateThrottle |
| from django.views.decorators.csrf import ensure_csrf_cookie |
| from django.utils.decorators import method_decorator |
| from rest_framework_simplejwt.tokens import RefreshToken |
| from rest_framework_simplejwt.exceptions import TokenError, InvalidToken |
| from django.contrib.auth import authenticate |
| from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView |
|
|
|
|
| def _set_auth_cookies(response: Response, refresh: RefreshToken) -> None: |
| """Write access and refresh tokens into HttpOnly cookies.""" |
| jwt_settings = settings.SIMPLE_JWT |
| secure = jwt_settings.get('AUTH_COOKIE_SECURE', not settings.DEBUG) |
| samesite = jwt_settings.get('AUTH_COOKIE_SAMESITE', 'Lax') |
|
|
| access_lifetime = jwt_settings.get('ACCESS_TOKEN_LIFETIME') |
| refresh_lifetime = jwt_settings.get('REFRESH_TOKEN_LIFETIME') |
|
|
| response.set_cookie( |
| key='access_token', |
| value=str(refresh.access_token), |
| max_age=int(access_lifetime.total_seconds()) if access_lifetime else 1800, |
| httponly=True, |
| secure=secure, |
| samesite=samesite, |
| path='/', |
| ) |
| response.set_cookie( |
| key='refresh_token', |
| value=str(refresh), |
| max_age=int(refresh_lifetime.total_seconds()) if refresh_lifetime else 604800, |
| httponly=True, |
| secure=secure, |
| samesite=samesite, |
| path='/api/auth/refresh/', |
| ) |
|
|
|
|
| def _clear_auth_cookies(response: Response) -> None: |
| """Delete auth cookies from the browser.""" |
| response.delete_cookie('access_token', path='/') |
| response.delete_cookie('refresh_token', path='/api/auth/refresh/') |
| response.delete_cookie('refresh_token', path='/api/token/refresh/') |
|
|
|
|
| from rest_framework.decorators import api_view, permission_classes, throttle_classes, authentication_classes |
|
|
| @api_view(['POST']) |
| @authentication_classes([]) |
| @permission_classes([AllowAny]) |
| @throttle_classes([ScopedRateThrottle]) |
| @ensure_csrf_cookie |
| def login_view(request): |
| """ |
| POST /api/auth/login/ |
| Body: { "username": "...", "password": "..." } |
| Only users with role='admin' can log in to the Dashboard. |
| Returns user info and sets HttpOnly cookies. |
| """ |
| request.throttle_scope = 'login' |
| username = request.data.get('username', '').strip() |
| password = request.data.get('password', '').strip() |
|
|
| if not username or not password: |
| return Response( |
| {'detail': 'Usuario y contraseña son requeridos.'}, |
| status=status.HTTP_400_BAD_REQUEST, |
| ) |
|
|
| user = authenticate(request, username=username, password=password) |
| |
| if user is None and '@' in username: |
| from django.contrib.auth import get_user_model |
| User = get_user_model() |
| try: |
| user_obj = User.objects.get(email=username) |
| user = authenticate(request, username=user_obj.username, password=password) |
| except User.DoesNotExist: |
| pass |
|
|
| if user is None: |
| return Response( |
| {'detail': 'Credenciales incorrectas. Verifica usuario y contraseña.'}, |
| status=status.HTTP_401_UNAUTHORIZED, |
| ) |
|
|
| if not getattr(user, 'role', None) == 'admin' and not user.is_superuser: |
| return Response( |
| {'detail': 'Acceso denegado. Esta área es solo para administradores.'}, |
| status=status.HTTP_403_FORBIDDEN, |
| ) |
|
|
| if not user.is_active: |
| return Response( |
| {'detail': 'Cuenta inactiva. Contacta al administrador.'}, |
| status=status.HTTP_401_UNAUTHORIZED, |
| ) |
|
|
| refresh = RefreshToken.for_user(user) |
| response = Response({ |
| 'username': user.username, |
| 'role': getattr(user, 'role', 'admin'), |
| 'first_name': user.first_name, |
| 'last_name': user.last_name, |
| }) |
| _set_auth_cookies(response, refresh) |
| return response |
|
|
|
|
| @api_view(['POST']) |
| @authentication_classes([]) |
| @permission_classes([AllowAny]) |
| def logout_view(request): |
| """ |
| POST /api/auth/logout/ |
| Clears auth cookies. No body required. |
| """ |
| response = Response({'detail': 'Sesión cerrada correctamente.'}) |
| _clear_auth_cookies(response) |
| return response |
|
|
|
|
| @api_view(['POST']) |
| @permission_classes([AllowAny]) |
| @throttle_classes([ScopedRateThrottle]) |
| def refresh_view(request): |
| """ |
| POST /api/auth/refresh/ |
| Reads refresh_token from HttpOnly cookie and issues a new access_token cookie. |
| """ |
| request.throttle_scope = 'login' |
| refresh_token = request.COOKIES.get('refresh_token') |
|
|
| if not refresh_token: |
| return Response( |
| {'detail': 'No hay token de refresco. Inicia sesión nuevamente.'}, |
| status=status.HTTP_401_UNAUTHORIZED, |
| ) |
|
|
| try: |
| refresh = RefreshToken(refresh_token) |
| user_id = refresh.get('user_id', None) |
| from django.contrib.auth import get_user_model |
| User = get_user_model() |
| user = User.objects.get(id=user_id) |
| |
| new_refresh = RefreshToken.for_user(user) |
| except (TokenError, InvalidToken, Exception): |
| response = Response( |
| {'detail': 'Token de refresco inválido o expirado. Inicia sesión nuevamente.'}, |
| status=status.HTTP_401_UNAUTHORIZED, |
| ) |
| _clear_auth_cookies(response) |
| return response |
|
|
| response = Response({'detail': 'Token renovado.'}) |
| _set_auth_cookies(response, new_refresh) |
| return response |
|
|
|
|
| @api_view(['GET']) |
| @ensure_csrf_cookie |
| @permission_classes([IsAuthenticated]) |
| def me_admin_view(request): |
| """ |
| GET /api/auth/me/ |
| Returns current authenticated admin user info. |
| Used by the Dashboard to rehydrate session on page reload. |
| """ |
| user = request.user |
| return Response({ |
| 'username': user.username, |
| 'role': getattr(user, 'role', 'user'), |
| 'first_name': user.first_name, |
| 'last_name': user.last_name, |
| 'is_superuser': user.is_superuser, |
| }) |
|
|
|
|
| @method_decorator(ensure_csrf_cookie, name='dispatch') |
| class CookieTokenObtainPairView(TokenObtainPairView): |
| throttle_classes = [ScopedRateThrottle] |
| throttle_scope = 'login' |
|
|
| 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') |
| |
| |
| response.data = {'detail': 'Sesión iniciada correctamente.'} |
| |
| jwt_settings = settings.SIMPLE_JWT |
| secure = jwt_settings.get('AUTH_COOKIE_SECURE', not settings.DEBUG) |
| samesite = jwt_settings.get('AUTH_COOKIE_SAMESITE', 'Lax') |
| access_lifetime = jwt_settings.get('ACCESS_TOKEN_LIFETIME') |
| refresh_lifetime = jwt_settings.get('REFRESH_TOKEN_LIFETIME') |
| |
| response.set_cookie( |
| key='access_token', |
| value=access, |
| max_age=int(access_lifetime.total_seconds()) if access_lifetime else 1800, |
| httponly=True, |
| secure=secure, |
| samesite=samesite, |
| path='/', |
| ) |
| response.set_cookie( |
| key='refresh_token', |
| value=refresh, |
| max_age=int(refresh_lifetime.total_seconds()) if refresh_lifetime else 604800, |
| httponly=True, |
| secure=secure, |
| samesite=samesite, |
| path='/api/token/refresh/', |
| ) |
| return response |
|
|
|
|
| class CookieTokenRefreshView(TokenRefreshView): |
| throttle_classes = [ScopedRateThrottle] |
| throttle_scope = 'login' |
|
|
| def post(self, request, *args, **kwargs): |
| refresh_token = request.COOKIES.get('refresh_token') |
| if not refresh_token: |
| return Response( |
| {'detail': 'Token de refresco faltante en cookies.'}, |
| status=status.HTTP_401_UNAUTHORIZED, |
| ) |
| request.data['refresh'] = refresh_token |
| response = super().post(request, *args, **kwargs) |
| if response.status_code == 200: |
| access = response.data.get('access') |
| refresh = response.data.get('refresh') |
| |
| |
| response.data = {'detail': 'Token renovado.'} |
| |
| jwt_settings = settings.SIMPLE_JWT |
| secure = jwt_settings.get('AUTH_COOKIE_SECURE', not settings.DEBUG) |
| samesite = jwt_settings.get('AUTH_COOKIE_SAMESITE', 'Lax') |
| access_lifetime = jwt_settings.get('ACCESS_TOKEN_LIFETIME') |
| |
| response.set_cookie( |
| key='access_token', |
| value=access, |
| max_age=int(access_lifetime.total_seconds()) if access_lifetime else 1800, |
| httponly=True, |
| secure=secure, |
| samesite=samesite, |
| path='/', |
| ) |
| |
| if refresh: |
| refresh_lifetime = jwt_settings.get('REFRESH_TOKEN_LIFETIME') |
| response.set_cookie( |
| key='refresh_token', |
| value=refresh, |
| max_age=int(refresh_lifetime.total_seconds()) if refresh_lifetime else 604800, |
| httponly=True, |
| secure=secure, |
| samesite=samesite, |
| path='/api/token/refresh/', |
| ) |
| return response |
|
|