diff --git a/apps/account/admin.py b/apps/account/admin.py index 938be965..dc88c34b 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -46,12 +46,3 @@ class UserAdmin(BaseUserAdmin): return obj.get_short_name() short_name.short_description = _('Name') - - -@admin.register(models.ResetPasswordToken) -class ResetPasswordToken(admin.ModelAdmin): - """Model admin for ResetPasswordToken""" - list_display = ('id', 'user', 'expiry_datetime') - list_filter = ('expiry_datetime', 'user') - search_fields = ('user', ) - readonly_fields = ('user', 'key', ) diff --git a/apps/account/migrations/0006_delete_resetpasswordtoken.py b/apps/account/migrations/0006_delete_resetpasswordtoken.py new file mode 100644 index 00000000..6b34bdb2 --- /dev/null +++ b/apps/account/migrations/0006_delete_resetpasswordtoken.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2.4 on 2019-09-12 11:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_user_cropped_image'), + ] + + operations = [ + migrations.DeleteModel( + name='ResetPasswordToken', + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 30255802..ca5a4b96 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -147,120 +147,43 @@ class User(ImageMixin, AbstractUser): @property def reset_password_token(self): """Make a token for finish signup.""" - return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self) + return password_token_generator.make_token(self) @property def get_user_uidb64(self): """Get base64 value for user by primary key identifier""" return urlsafe_base64_encode(force_bytes(self.pk)) - def confirm_email_template(self, country_code): - """Get confirm email template""" - return render_to_string( - template_name=settings.CONFIRM_EMAIL_TEMPLATE, - context={'token': self.confirm_email_token, - 'uidb64': self.get_user_uidb64, - 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME, - 'country_code': country_code}) - @property - def change_email_template(self): - """Get change email template""" - return render_to_string( - template_name=settings.CHANGE_EMAIL_TEMPLATE, - context={'token': self.change_email_token, - 'uidb64': self.get_user_uidb64, - 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME}) - - -class ResetPasswordTokenQuerySet(models.QuerySet): - """Reset password token query set""" - - def expired(self): - """Show only expired""" - return self.filter(expiry_datetime__lt=timezone.now()) - - def valid(self): - """Show only valid""" - return self.filter(expiry_datetime__gt=timezone.now()) - - def by_user(self, user): - """Show obj by user""" - return self.filter(user=user) - - -class ResetPasswordToken(PlatformMixin, ProjectBaseMixin): - """Reset password model""" - - user = models.ForeignKey(User, - related_name='password_reset_tokens', - on_delete=models.CASCADE, - verbose_name=_('The User which is associated to ' - 'this password reset token')) - # Key field, though it is not the primary key of the model - key = models.CharField(max_length=255, - verbose_name=_('Key')) - - ip_address = models.GenericIPAddressField(default='', - blank=True, null=True, - verbose_name=_('The IP address of this session')) - - expiry_datetime = models.DateTimeField(blank=True, null=True, - verbose_name=_('Expiration datetime')) - - objects = ResetPasswordTokenQuerySet.as_manager() - - class Meta: - verbose_name = _("Password Reset Token") - verbose_name_plural = _("Password Reset Tokens") - - def __str__(self): - return "Password reset token for user {user}".format(user=self.user) - - def save(self, *args, **kwargs): - """Override save method""" - if not self.expiry_datetime: - self.expiry_datetime = ( - timezone.now() + - timezone.timedelta(hours=self.get_resetting_token_expiration) - ) - if not self.key: - self.key = self.generate_token - return super(ResetPasswordToken, self).save(*args, **kwargs) - - @property - def get_resetting_token_expiration(self): - """Get resetting token expiration""" - return settings.RESETTING_TOKEN_EXPIRATION - - @property - def is_valid(self): - """Check if valid token or not""" - return timezone.now() > self.expiry_datetime - - @property - def generate_token(self): - """Generates a pseudo random code""" - return password_token_generator.make_token(self.user) + def base_template(self): + """Base email template""" + return {'domain_uri': settings.DOMAIN_URI, + 'uidb64': self.get_user_uidb64, + 'site_name': settings.SITE_NAME} def reset_password_template(self, country_code): """Get reset password template""" + context = {'token': self.reset_password_token, + 'country_code': country_code} + context.update(self.base_template) return render_to_string( template_name=settings.RESETTING_TOKEN_TEMPLATE, - context={'token': self.key, - 'uidb64': self.user.get_user_uidb64, - 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME, - 'country_code': country_code}) + context=context) - @staticmethod - def token_is_valid(user, token): - """Check if token is valid""" - return password_token_generator.check_token(user, token) + def confirm_email_template(self, country_code): + """Get confirm email template""" + context = {'token': self.confirm_email_token, + 'country_code': country_code} + context.update(self.base_template) + return render_to_string( + template_name=settings.CONFIRM_EMAIL_TEMPLATE, + context=context) - def overdue(self): - """Overdue instance""" - self.expiry_datetime = timezone.now() - self.save() + def change_email_template(self, country_code): + """Get change email template""" + context = {'token': self.change_email_token, + 'country_code': country_code} + context.update(self.base_template) + return render_to_string( + template_name=settings.CHANGE_EMAIL_TEMPLATE, + context=context) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index bd28cb88..e6599f96 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -48,7 +48,7 @@ class UserSerializer(serializers.ModelSerializer): def validate_email(self, value): """Validate email value""" if value == self.instance.email: - raise serializers.ValidationError() + raise serializers.ValidationError(detail='Equal email address.') return value def validate_username(self, value): @@ -58,24 +58,21 @@ class UserSerializer(serializers.ModelSerializer): raise utils_exceptions.NotValidUsernameError() return value - def validate(self, attrs): - if ('cropped_image' in attrs or 'image' in attrs) and \ - ('cropped_image' not in attrs or 'image' not in attrs): - raise utils_exceptions.UserUpdateUploadImageError() - return attrs - def update(self, instance, validated_data): - """ - Override update method - """ - if 'email' in validated_data: - validated_data['email_confirmed'] = False + """Override update method""" instance = super().update(instance, validated_data) - # Send verification link on user email for change email address - if settings.USE_CELERY: - tasks.confirm_new_email_address.delay(instance.id) - else: - tasks.confirm_new_email_address(instance.id) + if 'email' in validated_data: + instance.email_confirmed = False + instance.save() + # Send verification link on user email for change email address + if settings.USE_CELERY: + tasks.change_email_address.delay( + user_id=instance.id, + country_code=self.context.get('request').country_code) + else: + tasks.change_email_address( + user_id=instance.id, + country_code=self.context.get('request').country_code) return instance @@ -163,7 +160,7 @@ class ConfirmEmailSerializer(serializers.ModelSerializer): """Override validate method""" email_confirmed = self.instance.email_confirmed if email_confirmed: - raise serializers.ValidationError() + raise utils_exceptions.EmailConfirmedError() return attrs def update(self, instance, validated_data): diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index cdf616dc..d325cea6 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -1,27 +1,18 @@ """Serializers for account web""" -from django.conf import settings from django.contrib.auth import password_validation as password_validators -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from account import models, tasks +from account import models from utils import exceptions as utils_exceptions from utils.methods import username_validator -class PasswordResetSerializer(serializers.ModelSerializer): +class PasswordResetSerializer(serializers.Serializer): """Serializer from model PasswordReset""" username_or_email = serializers.CharField(required=False, write_only=True,) - class Meta: - """Meta class""" - model = models.ResetPasswordToken - fields = ( - 'username_or_email', - ) - @property def request(self): """Get request from context""" @@ -30,41 +21,29 @@ class PasswordResetSerializer(serializers.ModelSerializer): def validate(self, attrs): """Override validate method""" user = self.request.user + username_or_email = attrs.get('username_or_email') - if user.is_anonymous: - username_or_email = attrs.get('username_or_email') + if not user.is_authenticated: if not username_or_email: - raise serializers.ValidationError(_('Username or Email not in request body.')) - # Check user in DB + raise serializers.ValidationError(_('username or email not in request body.')) + filters = {} if username_validator(username_or_email): - filters.update({'username': username_or_email}) + filters.update({'username__icontains': username_or_email}) else: - filters.update({'email': username_or_email.lower()}) - user_qs = models.User.objects.filter(**filters) - if user_qs.exists() and filters: - attrs['user'] = user_qs.first() - else: - attrs['user'] = user + filters.update({'email__icontains': username_or_email}) + + if filters: + filters.update({'is_active': True}) + user_qs = models.User.objects.filter(**filters) + + if not user_qs.exists(): + raise utils_exceptions.UserNotFoundError() + user = user_qs.first() + + attrs['user'] = user return attrs - def create(self, validated_data, *args, **kwargs): - """Override create method""" - user = validated_data.pop('user') - ip_address = self.request.META.get('REMOTE_ADDR') - - obj = models.ResetPasswordToken.objects.create( - user=user, - ip_address=ip_address, - source=models.ResetPasswordToken.WEB) - if settings.USE_CELERY: - tasks.send_reset_password_email.delay(request_id=obj.id, - country_code=self.request.country_code) - else: - tasks.send_reset_password_email(request_id=obj.id, - country_code=self.request.country_code) - return obj - class PasswordResetConfirmSerializer(serializers.ModelSerializer): """Serializer for model User""" @@ -73,30 +52,24 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer): class Meta: """Meta class""" - model = models.ResetPasswordToken + model = models.User fields = ('password', ) - def validate(self, attrs): - """Override validate method""" - user = self.instance.user - password = attrs.get('password') + def validate_password(self, value): + """Password validation method.""" try: # Compare new password with the old ones - if user.check_password(raw_password=password): + if self.instance.check_password(raw_password=value): raise utils_exceptions.PasswordsAreEqual() # Validate password - password_validators.validate_password(password=password) + password_validators.validate_password(password=value) except serializers.ValidationError as e: raise serializers.ValidationError(str(e)) - else: - return attrs + return value def update(self, instance, validated_data): """Override update method""" # Update user password from instance - instance.user.set_password(validated_data.get('password')) - instance.user.save() - - # Overdue instance - instance.overdue() + instance.set_password(validated_data.get('password')) + instance.save() return instance diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 362daddf..03a231b3 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -11,26 +11,37 @@ logger = logging.getLogger(__name__) @shared_task -def send_reset_password_email(request_id, country_code): +def send_reset_password_email(user_id, country_code): """Send email to user for reset password.""" try: - obj = models.ResetPasswordToken.objects.get(id=request_id) - user = obj.user + user = models.User.objects.get(id=user_id) user.send_email(subject=_('Password resetting'), - message=obj.reset_password_template(country_code)) + message=user.reset_password_template(country_code)) except: logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' - f'DETAIL: Exception occurred for ResetPasswordToken instance: ' - f'{request_id}') + f'DETAIL: Exception occurred for reset password: ' + f'{user_id}') @shared_task -def confirm_new_email_address(user_id): +def confirm_new_email_address(user_id, country_code): """Send email to user new email.""" try: user = models.User.objects.get(id=user_id) user.send_email(subject=_('Validate new email address'), - message=user.change_email_template) + message=user.confirm_email_template(country_code)) except: logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') + + +@shared_task +def change_email_address(user_id, country_code): + """Send email to user new email.""" + try: + user = models.User.objects.get(id=user_id) + user.send_email(subject=_('Validate new email address'), + message=user.change_email_template(country_code)) + except: + logger.error(f'METHOD_NAME: {change_email_address.__name__}\n' + f'DETAIL: Exception occurred for user: {user_id}') diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 0e8ae835..34583010 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -8,7 +8,5 @@ app_name = 'account' urlpatterns = [ path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'), path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), - path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), - name='change-email-confirm'), - path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), + path('email/confirm///', views.ConfirmEmailView.as_view(), name='confirm-email'), ] diff --git a/apps/account/urls/web.py b/apps/account/urls/web.py index cc57f316..e590e76d 100644 --- a/apps/account/urls/web.py +++ b/apps/account/urls/web.py @@ -8,10 +8,8 @@ app_name = 'account' urlpatterns_api = [ path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'), - path('form/reset-password///', views.FormPasswordResetConfirmView.as_view(), - name='form-password-reset-confirm'), - path('form/reset-password/success/', views.FormPasswordResetSuccessView.as_view(), - name='form-password-reset-success'), + path('reset-password/confirm///', views.PasswordResetConfirmView.as_view(), + name='password-reset-confirm'), ] urlpatterns = urlpatterns_api + \ diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 66ad3fa0..ab62343f 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -6,14 +6,12 @@ from rest_framework import generics from rest_framework import permissions from rest_framework import status from rest_framework.response import Response -from django.shortcuts import get_object_or_404 from account import models from account.serializers import common as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator -from utils.views import (JWTUpdateAPIView, - JWTGenericViewMixin) +from utils.views import JWTGenericViewMixin # User views @@ -26,53 +24,27 @@ class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): return self.request.user -class ChangePasswordView(JWTUpdateAPIView): +class ChangePasswordView(generics.GenericAPIView): """Change password view""" serializer_class = serializers.ChangePasswordSerializer queryset = models.User.objects.active() - permission_classes = (permissions.AllowAny, ) - - def get_object(self): - """Overridden get_object method.""" - if not self.request.user.is_authenticated: - uidb64 = self.kwargs.get('uidb64') - - user_id = force_text(urlsafe_base64_decode(uidb64)) - token = self.kwargs.get('token') - - filter_kwargs = {'key': token, 'user_id': user_id} - password_reset_obj = get_object_or_404(models.ResetPasswordToken.objects.valid(), - **filter_kwargs) - if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( - user=password_reset_obj.user, token=token): - raise utils_exceptions.NotValidAccessTokenError() - if not password_reset_obj.user.is_active: - raise utils_exceptions.UserNotFoundError() - obj = password_reset_obj.user - else: - obj = self.request.user - - # May raise a permission denied - self.check_object_permissions(self.request, obj) - return obj def patch(self, request, *args, **kwargs): """Implement PUT method""" - instance = self.get_object() - serializer = self.get_serializer(instance=instance, + serializer = self.get_serializer(instance=self.request.user, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return Response(status=status.HTTP_200_OK) -class ConfirmEmailView(JWTGenericViewMixin): +class SendConfirmationEmailView(JWTGenericViewMixin): """Confirm email view.""" serializer_class = serializers.ConfirmEmailSerializer queryset = models.User.objects.all() def patch(self, request, *args, **kwargs): - """Implement POST-method""" + """Implement PATCH-method""" # Get user instance instance = self.request.user @@ -82,7 +54,7 @@ class ConfirmEmailView(JWTGenericViewMixin): return Response(status=status.HTTP_200_OK) -class ChangeEmailConfirmView(JWTGenericViewMixin): +class ConfirmEmailView(JWTGenericViewMixin): """View for confirm changing email""" permission_classes = (permissions.AllowAny,) @@ -95,12 +67,18 @@ class ChangeEmailConfirmView(JWTGenericViewMixin): user_qs = models.User.objects.filter(pk=uid) if user_qs.exists(): user = user_qs.first() - if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( + if not GMTokenGenerator(GMTokenGenerator.CONFIRM_EMAIL).check_token( user, token): raise utils_exceptions.NotValidTokenError() # Approve email status user.confirm_email() - return Response(status=status.HTTP_200_OK) + # Create tokens + tokens = user.create_jwt_tokens() + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies( + access_token=tokens.get('access_token'), + refresh_token=tokens.get('refresh_token')), + response=Response(status=status.HTTP_200_OK)) else: raise utils_exceptions.UserNotFoundError() diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 4f10ccc2..e4596e9f 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -1,42 +1,35 @@ """Web account views""" from django.conf import settings -from django.contrib.auth.tokens import default_token_generator -from django.core.exceptions import ValidationError -from django.http import HttpResponseRedirect +from django.contrib.auth.tokens import default_token_generator as password_token_generator from django.shortcuts import get_object_or_404 -from django.urls import reverse_lazy -from django.utils.decorators import method_decorator from django.utils.encoding import force_text from django.utils.http import urlsafe_base64_decode -from django.utils.translation import gettext_lazy as _ -from django.views.decorators.cache import never_cache -from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.edit import FormView -from rest_framework import permissions -from rest_framework import status -from rest_framework import views +from rest_framework import permissions, status, generics from rest_framework.response import Response -from account import models -from account.forms import SetPasswordForm +from account import tasks, models from account.serializers import web as serializers from utils import exceptions as utils_exceptions -from utils.models import GMTokenGenerator from utils.views import JWTGenericViewMixin -class PasswordResetView(JWTGenericViewMixin): +class PasswordResetView(generics.GenericAPIView): """View for resetting user password""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.PasswordResetSerializer - queryset = models.ResetPasswordToken.objects.valid() def post(self, request, *args, **kwargs): """Override create method""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) if serializer.validated_data.get('user'): - serializer.save() + user = serializer.validated_data.pop('user') + if settings.USE_CELERY: + tasks.send_reset_password_email.delay(user_id=user.id, + country_code=self.request.country_code) + else: + tasks.send_reset_password_email(user_id=user.id, + country_code=self.request.country_code) return Response(status=status.HTTP_200_OK) @@ -44,10 +37,7 @@ class PasswordResetConfirmView(JWTGenericViewMixin): """View for confirmation new password""" serializer_class = serializers.PasswordResetConfirmSerializer permission_classes = (permissions.AllowAny,) - - def get_queryset(self): - """Override get_queryset method""" - return models.ResetPasswordToken.objects.valid() + queryset = models.User.objects.active() def get_object(self): """Override get_object method @@ -58,128 +48,27 @@ class PasswordResetConfirmView(JWTGenericViewMixin): user_id = force_text(urlsafe_base64_decode(uidb64)) token = self.kwargs.get('token') - filter_kwargs = {'key': token, 'user_id': user_id} - obj = get_object_or_404(queryset, **filter_kwargs) + obj = get_object_or_404(queryset, id=user_id) - if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( - user=obj.user, token=token): - raise utils_exceptions.NotValidAccessTokenError() + if not password_token_generator.check_token(user=obj, token=token): + raise utils_exceptions.NotValidTokenError() # May raise a permission denied self.check_object_permissions(self.request, obj) return obj - def put(self, request, *args, **kwargs): - """Implement PUT method""" + def patch(self, request, *args, **kwargs): + """Implement PATCH method""" instance = self.get_object() serializer = self.get_serializer(instance=instance, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response(status=status.HTTP_200_OK) - - -# Form view -class PasswordContextMixin: - extra_context = None - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'title': self.title, - **(self.extra_context or {}) - }) - return context - - -class FormPasswordResetSuccessView(views.APIView): - """View for successful reset password""" - - permission_classes = (permissions.AllowAny, ) - - def get(self, request, *args, **kwargs): - """Implement GET-method""" - return Response(status=status.HTTP_200_OK) - - -class FormPasswordResetConfirmView(PasswordContextMixin, FormView): - - INTERNAL_RESET_URL_TOKEN = 'set-password' - INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token' - - form_class = SetPasswordForm - post_reset_login = False - post_reset_login_backend = None - success_url = reverse_lazy('web:account:form-password-reset-success') - template_name = settings.CONFIRMATION_PASSWORD_RESET_TEMPLATE - title = _('Enter new password') - token_generator = default_token_generator - - @method_decorator(sensitive_post_parameters()) - @method_decorator(never_cache) - def dispatch(self, *args, **kwargs): - assert 'uidb64' in kwargs and 'token' in kwargs - - self.validlink = False - self.user = self.get_user(kwargs['uidb64']) - - if self.user is not None: - token = kwargs['token'] - if token == self.INTERNAL_RESET_URL_TOKEN: - session_token = self.request.session.get(self.INTERNAL_RESET_SESSION_TOKEN) - if self.token_generator.check_token(self.user, session_token): - # If the token is valid, display the password reset form. - self.validlink = True - return super().dispatch(*args, **kwargs) - else: - if self.token_generator.check_token(self.user, token): - # Store the token in the session and redirect to the - # password reset form at a URL without the token. That - # avoids the possibility of leaking the token in the - # HTTP Referer header. - self.request.session[self.INTERNAL_RESET_SESSION_TOKEN] = token - redirect_url = self.request.path.replace(token, self.INTERNAL_RESET_URL_TOKEN) - return HttpResponseRedirect(redirect_url) - - # Display the "Password reset unsuccessful" page. - return self.render_to_response(self.get_context_data()) - - def get_user(self, uidb64): - try: - # urlsafe_base64_decode() decodes to bytestring - uid = urlsafe_base64_decode(uidb64).decode() - user = models.User.objects.get(pk=uid) - except (TypeError, ValueError, OverflowError, models.User.DoesNotExist, ValidationError): - user = None - return user - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['user'] = self.user - return kwargs - - def form_valid(self, form): - # Saving form - form.save() - user = form.user - - # Expire user tokens - user.expire_access_tokens() - user.expire_refresh_tokens() - - # Pop session token - del self.request.session[self.INTERNAL_RESET_SESSION_TOKEN] - return super().form_valid(form) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if self.validlink: - context['validlink'] = True - else: - context.update({ - 'form': None, - 'title': _('Password reset unsuccessful'), - 'validlink': False, - }) - return context + # Create tokens + tokens = instance.create_jwt_tokens() + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies( + access_token=tokens.get('access_token'), + refresh_token=tokens.get('refresh_token')), + response=Response(status=status.HTTP_200_OK)) diff --git a/apps/authorization/urls/common.py b/apps/authorization/urls/common.py index 616f9d99..4e6e59e1 100644 --- a/apps/authorization/urls/common.py +++ b/apps/authorization/urls/common.py @@ -29,7 +29,7 @@ urlpatterns_oauth2 = [ urlpatterns_jwt = [ path('signup/', views.SignUpView.as_view(), name='signup'), - path('signup/confirm///', views.VerifyEmailConfirmView.as_view(), + path('signup/confirm///', views.ConfirmationEmailView.as_view(), name='signup-confirm'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), path('logout/', views.LogoutView.as_view(), name="logout"), diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 827ff108..98f79bd9 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -24,13 +24,12 @@ from authorization.serializers import common as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator from utils.permissions import IsAuthenticatedAndTokenIsValid -from utils.views import (JWTGenericViewMixin, - JWTCreateAPIView) +from utils.views import JWTGenericViewMixin # Mixins # JWTAuthView mixin -class JWTAuthViewMixin(JWTCreateAPIView): +class JWTAuthViewMixin(JWTGenericViewMixin): """Mixin for authentication views""" def post(self, request, *args, **kwargs): @@ -151,7 +150,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin): # JWT # Sign in via username and password -class SignUpView(JWTCreateAPIView): +class SignUpView(generics.GenericAPIView): """View for classic signup""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.SignupSerializer @@ -164,7 +163,7 @@ class SignUpView(JWTCreateAPIView): return Response(status=status.HTTP_201_CREATED) -class VerifyEmailConfirmView(JWTGenericViewMixin): +class ConfirmationEmailView(JWTGenericViewMixin): """View for confirmation email""" permission_classes = (permissions.AllowAny, ) diff --git a/apps/news/views/common.py b/apps/news/views/common.py index 2678289b..cf3c7e29 100644 --- a/apps/news/views/common.py +++ b/apps/news/views/common.py @@ -1,8 +1,9 @@ """News app common app.""" from rest_framework import generics, permissions + from news import filters, models from news.serializers import common as serializers -from utils.views import JWTGenericViewMixin, JWTListAPIView +from utils.views import JWTGenericViewMixin class NewsMixin: @@ -18,7 +19,7 @@ class NewsMixin: .order_by('-is_highlighted', '-created') -class NewsListView(NewsMixin, JWTListAPIView): +class NewsListView(NewsMixin, generics.ListAPIView): """News list view.""" filter_class = filters.NewsListFilterSet diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 5251c4e4..df9c2c27 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -32,15 +32,6 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException): default_detail = _('User not found') -class PasswordRequestResetExists(ProjectBaseException): - """ - The exception should be thrown when request for reset password - is already exists and valid - """ - status_code = status.HTTP_400_BAD_REQUEST - default_detail = _('Password request is already exists. Please wait.') - - class EmailSendingError(exceptions.APIException): """The exception should be thrown when unable to send an email""" status_code = status.HTTP_400_BAD_REQUEST @@ -142,3 +133,12 @@ class FavoritesError(exceptions.APIException): """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Item is already in favorites.') + + +class PasswordResetRequestExistedError(exceptions.APIException): + """ + The exception should be thrown when password reset request + already exists and valid. + """ + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Password reset request is already exists and valid.') diff --git a/apps/utils/views.py b/apps/utils/views.py index d129fb73..9af89360 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response # JWT -# Login base view mixin +# Login base view mixins class JWTGenericViewMixin(generics.GenericAPIView): """JWT view mixin""" @@ -95,102 +95,3 @@ class JWTGenericViewMixin(generics.GenericAPIView): http_only=self.REFRESH_TOKEN_HTTP_ONLY, secure=self.REFRESH_TOKEN_SECURE, max_age=_cookies.get('max_age'))] - - -class JWTListAPIView(JWTGenericViewMixin, generics.ListAPIView): - """ - Concrete view for creating a model instance. - """ - def get(self, request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - response = self.get_paginated_response(serializer.data) - else: - serializer = self.get_serializer(queryset, many=True) - response = Response(serializer.data) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token.value, - refresh_token=refresh_token.value), - response=response) - - -class JWTCreateAPIView(JWTGenericViewMixin, generics.CreateAPIView): - """ - Concrete view for creating a model instance. - """ - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save() - response = Response(serializer.data, status=status.HTTP_201_CREATED) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token.value, - refresh_token=refresh_token.value), - response=response) - - -class JWTRetrieveAPIView(JWTGenericViewMixin, generics.RetrieveAPIView): - """ - Concrete view for retrieving a model instance. - """ - def get(self, request, *args, **kwargs): - """Implement GET method""" - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - response = self.get_paginated_response(serializer.data) - else: - serializer = self.get_serializer(queryset, many=True) - response = Response(serializer.data, status.HTTP_200_OK) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - -class JWTDestroyAPIView(JWTGenericViewMixin, generics.DestroyAPIView): - """ - Concrete view for deleting a model instance. - """ - def delete(self, request, *args, **kwargs): - instance = self.get_object() - instance.delete() - response = Response(status=status.HTTP_204_NO_CONTENT) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - -class JWTUpdateAPIView(JWTGenericViewMixin, generics.UpdateAPIView): - """ - Concrete view for updating a model instance. - """ - def put(self, request, *args, **kwargs): - partial = kwargs.pop('partial', False) - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=partial) - serializer.is_valid(raise_exception=True) - serializer.save() - if getattr(instance, '_prefetched_objects_cache', None): - # If 'prefetch_related' has been applied to a queryset, we need to - # forcibly invalidate the prefetch cache on the instance. - instance._prefetched_objects_cache = {} - response = Response(serializer.data) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.put(request, *args, **kwargs) - diff --git a/project/settings/base.py b/project/settings/base.py index b7047f98..16ac6002 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -369,7 +369,6 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1 # TEMPLATES -CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html' RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html' CHANGE_EMAIL_TEMPLATE = 'account/change_email.html' CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html' diff --git a/project/templates/account/change_email.html b/project/templates/account/change_email.html index ceec753f..40c1b227 100644 --- a/project/templates/account/change_email.html +++ b/project/templates/account/change_email.html @@ -2,9 +2,8 @@ {% blocktrans %}You're receiving this email because you want to change email address at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page for confirmation new email address:" %} -{% block reset_link %} -http://{{ domain_uri }}{% url 'web:account:change-email-confirm' uidb64=uidb64 token=token %} -{% endblock %} + +https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} diff --git a/project/templates/account/password_reset_confirm.html b/project/templates/account/password_reset_confirm.html deleted file mode 100644 index 62cdb8eb..00000000 --- a/project/templates/account/password_reset_confirm.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load i18n static %} - -{% block content %} - -{% if validlink %} - -

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

- -
{% csrf_token %} -
-
- {{ form.new_password1.errors }} - - {{ form.new_password1 }} -
-
- {{ form.new_password2.errors }} - - {{ form.new_password2 }} -
- -
-
- -{% else %} - -

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

- -{% endif %} - -{% endblock %} \ No newline at end of file diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index c32469f7..ee84fd0b 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -2,7 +2,9 @@ {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %} + https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/ + {% trans "Thanks for using our site!" %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %}