from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth import login as django_login from django.contrib.auth import logout as django_logout from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.views.decorators.debug import sensitive_post_parameters from rest_framework import status from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from .app_settings import api_settings from .models import get_token_model from .utils import jwt_encode sensitive_post_parameters_m = method_decorator( sensitive_post_parameters( 'password', 'old_password', 'new_password1', 'new_password2', ), ) class LoginView(GenericAPIView): """ Check the credentials and return the REST Token if the credentials are valid and authenticated. Calls Django Auth login method to register User ID in Django session framework Accept the following POST parameters: username, password Return the REST Framework Token Object's key. """ permission_classes = (AllowAny,) serializer_class = api_settings.LOGIN_SERIALIZER throttle_scope = 'dj_rest_auth' user = None access_token = None token = None @sensitive_post_parameters_m def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) def process_login(self): django_login(self.request, self.user) def get_response_serializer(self): if api_settings.USE_JWT: if api_settings.JWT_AUTH_RETURN_EXPIRATION: response_serializer = api_settings.JWT_SERIALIZER_WITH_EXPIRATION else: response_serializer = api_settings.JWT_SERIALIZER else: response_serializer = api_settings.TOKEN_SERIALIZER return response_serializer def login(self): self.user = self.serializer.validated_data['user'] token_model = get_token_model() if api_settings.USE_JWT: self.access_token, self.refresh_token = jwt_encode(self.user) elif token_model: self.token = api_settings.TOKEN_CREATOR(token_model, self.user, self.serializer) if api_settings.SESSION_LOGIN: self.process_login() def get_response(self): serializer_class = self.get_response_serializer() if api_settings.USE_JWT: from rest_framework_simplejwt.settings import ( api_settings as jwt_settings, ) access_token_expiration = (timezone.now() + jwt_settings.ACCESS_TOKEN_LIFETIME) refresh_token_expiration = (timezone.now() + jwt_settings.REFRESH_TOKEN_LIFETIME) return_expiration_times = api_settings.JWT_AUTH_RETURN_EXPIRATION auth_httponly = api_settings.JWT_AUTH_HTTPONLY data = { 'user': self.user, 'access': self.access_token, } if not auth_httponly: data['refresh'] = self.refresh_token else: # Wasnt sure if the serializer needed this data['refresh'] = "" if return_expiration_times: data['access_expiration'] = access_token_expiration data['refresh_expiration'] = refresh_token_expiration serializer = serializer_class( instance=data, context=self.get_serializer_context(), ) elif self.token: serializer = serializer_class( instance=self.token, context=self.get_serializer_context(), ) else: return Response(status=status.HTTP_204_NO_CONTENT) response = Response(serializer.data, status=status.HTTP_200_OK) if api_settings.USE_JWT: from .jwt_auth import set_jwt_cookies set_jwt_cookies(response, self.access_token, self.refresh_token) return response def post(self, request, *args, **kwargs): self.request = request self.serializer = self.get_serializer(data=self.request.data) self.serializer.is_valid(raise_exception=True) self.login() return self.get_response() class LogoutView(APIView): """ Calls Django logout method and delete the Token object assigned to the current User object. Accepts/Returns nothing. """ permission_classes = (AllowAny,) throttle_scope = 'dj_rest_auth' def get(self, request, *args, **kwargs): if getattr(settings, 'ACCOUNT_LOGOUT_ON_GET', False): response = self.logout(request) else: response = self.http_method_not_allowed(request, *args, **kwargs) return self.finalize_response(request, response, *args, **kwargs) def post(self, request, *args, **kwargs): return self.logout(request) def logout(self, request): if not (request.auth or api_settings.USE_JWT or api_settings.SESSION_LOGIN): return Response( {'detail': _('You should be logged in to logout. Check whether the token is passed.')}, status=status.HTTP_400_BAD_REQUEST, ) try: request.user.auth_token.delete() except (AttributeError, ObjectDoesNotExist): pass if api_settings.SESSION_LOGIN: django_logout(request) response = Response( {'detail': _('Successfully logged out.')}, status=status.HTTP_200_OK, ) if api_settings.USE_JWT: # NOTE: this import occurs here rather than at the top level # because JWT support is optional, and if `USE_JWT` isn't # True we shouldn't need the dependency from rest_framework_simplejwt.exceptions import TokenError from rest_framework_simplejwt.tokens import RefreshToken from .jwt_auth import unset_jwt_cookies cookie_name = api_settings.JWT_AUTH_COOKIE unset_jwt_cookies(response) if 'rest_framework_simplejwt.token_blacklist' in settings.INSTALLED_APPS: # add refresh token to blacklist try: token: RefreshToken = RefreshToken(None) if api_settings.JWT_AUTH_HTTPONLY: try: token = RefreshToken(request.COOKIES[api_settings.JWT_AUTH_REFRESH_COOKIE]) except KeyError: response.data = {'detail': _('Refresh token was not included in cookie data.')} response.status_code = status.HTTP_401_UNAUTHORIZED else: try: token = RefreshToken(request.data['refresh']) except KeyError: response.data = {'detail': _('Refresh token was not included in request data.')} response.status_code = status.HTTP_401_UNAUTHORIZED token.blacklist() except TokenError as error: response.data = {'detail': _(str(error))} response.status_code = status.HTTP_401_UNAUTHORIZED except (AttributeError, TypeError): response.data = {'detail': _('An error has occurred.')} response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR elif not cookie_name: message = _( 'Neither cookies or blacklist are enabled, so the token ' 'has not been deleted server side. Please make sure the token is deleted client side.', ) response.data = {'detail': message} response.status_code = status.HTTP_200_OK return response class UserDetailsView(RetrieveUpdateAPIView): """ Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. """ serializer_class = api_settings.USER_DETAILS_SERIALIZER permission_classes = (IsAuthenticated,) def get_object(self): return self.request.user def get_queryset(self): """ Adding this method since it is sometimes called when using django-rest-swagger """ return get_user_model().objects.none() class PasswordResetView(GenericAPIView): """ Calls Django Auth PasswordResetForm save method. Accepts the following POST parameters: email Returns the success/fail message. """ serializer_class = api_settings.PASSWORD_RESET_SERIALIZER permission_classes = (AllowAny,) throttle_scope = 'dj_rest_auth' def post(self, request, *args, **kwargs): # Create a serializer with request.data serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() # Return the success message with OK HTTP status return Response( {'detail': _('Password reset e-mail has been sent.')}, status=status.HTTP_200_OK, ) class PasswordResetConfirmView(GenericAPIView): """ Password reset e-mail link is confirmed, therefore this resets the user's password. Accepts the following POST parameters: token, uid, new_password1, new_password2 Returns the success/fail message. """ serializer_class = api_settings.PASSWORD_RESET_CONFIRM_SERIALIZER permission_classes = (AllowAny,) throttle_scope = 'dj_rest_auth' @sensitive_post_parameters_m def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return Response( {'detail': _('Password has been reset with the new password.')}, ) class PasswordChangeView(GenericAPIView): """ Calls Django Auth SetPasswordForm save method. Accepts the following POST parameters: new_password1, new_password2 Returns the success/fail message. """ serializer_class = api_settings.PASSWORD_CHANGE_SERIALIZER permission_classes = (IsAuthenticated,) throttle_scope = 'dj_rest_auth' @sensitive_post_parameters_m def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return Response({'detail': _('New password has been saved.')})