""" 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) # Force rotate: generate a new refresh token 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') # Clear response data so javascript cannot read them 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') # Clear response data so javascript cannot read them 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