diff --git a/apps/account/migrations/0009_user_unconfirmed_email.py b/apps/account/migrations/0009_user_unconfirmed_email.py new file mode 100644 index 00000000..6aaf3a6a --- /dev/null +++ b/apps/account/migrations/0009_user_unconfirmed_email.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-10 14:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0008_auto_20190912_1325'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='unconfirmed_email', + field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='unconfirmed email'), + ), + ] diff --git a/apps/account/migrations/0010_user_password_confirmed.py b/apps/account/migrations/0010_user_password_confirmed.py new file mode 100644 index 00000000..5f369f3c --- /dev/null +++ b/apps/account/migrations/0010_user_password_confirmed.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-10 17:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0009_user_unconfirmed_email'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='password_confirmed', + field=models.BooleanField(default=True, verbose_name='is new password confirmed'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 4ba03521..cffc3d45 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -60,8 +60,10 @@ class User(AbstractUser): blank=True, null=True, default=None) email = models.EmailField(_('email address'), unique=True, null=True, default=None) + unconfirmed_email = models.EmailField(_('unconfirmed email'), blank=True, null=True, default=None) email_confirmed = models.BooleanField(_('email status'), default=False) newsletter = models.NullBooleanField(default=True) + password_confirmed = models.BooleanField(_('is new password confirmed'), default=True, null=False) EMAIL_FIELD = 'email' USERNAME_FIELD = 'username' @@ -112,9 +114,15 @@ class User(AbstractUser): def confirm_email(self): """Method to confirm user email address""" + self.email = self.unconfirmed_email + self.unconfirmed_email = None self.email_confirmed = True self.save() + def confirm_password(self): + self.password_confirmed = True + self.save() + def approve(self): """Set user is_active status to True""" self.is_active = True @@ -149,6 +157,11 @@ class User(AbstractUser): """Make a token for finish signup.""" return password_token_generator.make_token(self) + @property + def confirm_password_token(self): + """Make a token for new password confirmation """ + return GMTokenGenerator(purpose=GMTokenGenerator.CONFIRM_PASSWORD).make_token(self) + @property def get_user_uidb64(self): """Get base64 value for user by primary key identifier""" @@ -178,6 +191,16 @@ class User(AbstractUser): template_name=settings.RESETTING_TOKEN_TEMPLATE, context=context) + def confirm_password_template(self, country_code): + """Get confirm password template""" + context = {'token': self.confirm_password_token, + 'country_code': country_code} + context.update(self.base_template) + return render_to_string( + template_name=settings.CONFIRM_PASSWORD_TEMPLATE, + context=context, + ) + def confirm_email_template(self, country_code): """Get confirm email template""" context = {'token': self.confirm_email_token, diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index b68aca7d..ad232eae 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -61,9 +61,12 @@ class UserSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): """Override update method""" + old_email = instance.email instance = super().update(instance, validated_data) if 'email' in validated_data: instance.email_confirmed = False + instance.email = old_email + instance.unconfirmed_email = validated_data['email'] instance.save() # Send verification link on user email for change email address if settings.USE_CELERY: diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 8be73afa..dd8ccec8 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -1,8 +1,10 @@ """Serializers for account web""" from django.contrib.auth import password_validation as password_validators +from django.conf import settings from rest_framework import serializers from account import models +from account import tasks from utils import exceptions as utils_exceptions from utils.methods import username_validator @@ -67,5 +69,16 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer): """Override update method""" # Update user password from instance instance.set_password(validated_data.get('password')) + instance.password_confirmed = False instance.save() + if settings.USE_CELERY: + tasks.send_reset_password_confirm.delay( + user=instance, + country_code=self.context.get('request').country_code, + ) + else: + tasks.send_reset_password_confirm( + user=instance, + country_code=self.context.get('request').country_code, + ) return instance diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 13c6f594..6eab7c85 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -22,6 +22,17 @@ def send_reset_password_email(user_id, country_code): f'DETAIL: Exception occurred for reset password: ' f'{user_id}') +@shared_task +def send_reset_password_confirm(user: models.User, country_code): + """ Send email to user for applying new password. """ + try: + user.send_email(subject=_('New password confirmation'), + message=user.confirm_password_template(country_code)) + except: + logger.error(f'METHOD_NAME: {send_reset_password_confirm.__name__}\n' + f'DETAIL: Exception occured for new passwordconfirmation', + f'{user.id}') + @shared_task def confirm_new_email_address(user_id, country_code): diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 4ea2af66..a440c5bf 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -8,6 +8,7 @@ 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-password-confirm///', views.ConfirmPasswordView.as_view(), name='change-password'), path('email/confirm/', views.SendConfirmationEmailView.as_view(), name='send-confirm-email'), path('email/confirm///', views.ConfirmEmailView.as_view(), name='confirm-email'), ] diff --git a/apps/account/views/common.py b/apps/account/views/common.py index d29ce2bb..cb0d84d7 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -91,6 +91,32 @@ class ConfirmEmailView(JWTGenericViewMixin): else: raise utils_exceptions.UserNotFoundError() +class ConfirmPasswordView(JWTGenericViewMixin): + """View for applying newly set password""" + + permission_classes = (permissions.AllowAny,) + + def get(self, request, *args, **kwargs): + uidb64 = kwargs.get('uidb64') + token = kwargs.get('token') + uid = force_text(urlsafe_base64_decode(uidb64)) + user_qs = models.User.objects.filter(pk=uid) + if user_qs.exists(): + user = user_qs.first() + if not GMTokenGenerator(GMTokenGenerator.CONFIRM_PASSWORD).check_token( + user, token): + raise utils_exceptions.NotValidTokenError() + user.confirm_password() + 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() + + # Firebase Cloud Messaging class FCMDeviceViewSet(generics.GenericAPIView): diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 13121f78..5beec2af 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -108,8 +108,8 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin, """Override validate method""" username_or_email = attrs.pop('username_or_email') password = attrs.pop('password') - user_qs = account_models.User.objects.filter(Q(username=username_or_email) | - Q(email=username_or_email)) + user_qs = account_models.User.objects.filter(password_confirmed=True)\ + .filter(Q(username=username_or_email) | Q(email=username_or_email)) if not user_qs.exists(): raise utils_exceptions.WrongAuthCredentials() else: diff --git a/apps/booking/models/services.py b/apps/booking/models/services.py index fd685548..a3ee17ea 100644 --- a/apps/booking/models/services.py +++ b/apps/booking/models/services.py @@ -141,7 +141,7 @@ class LastableService(AbstractBookingService): super().check_whether_booking_available(restaurant_id, date) url = f'{self.url}v1/restaurant/{restaurant_id}/offers' r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies) - response = json.loads(r.content)['data'] + response = json.loads(r.content).get('data') if not status.is_success(r.status_code) or not response: return False self.response = response diff --git a/apps/booking/views.py b/apps/booking/views.py index 245dbf05..dfa64bb9 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -35,9 +35,9 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): response = { 'available': is_booking_available, - 'type': service.service, + 'type': service.service if service else None, } - response.update({'details': service.response} if service.response else {}) + response.update({'details': service.response} if service and service.response else {}) return Response(data=response, status=200) diff --git a/apps/utils/models.py b/apps/utils/models.py index fb1de17c..710dce5c 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -299,12 +299,14 @@ class GMTokenGenerator(PasswordResetTokenGenerator): RESET_PASSWORD = 1 CHANGE_PASSWORD = 2 CONFIRM_EMAIL = 3 + CONFIRM_PASSWORD = 4 TOKEN_CHOICES = ( CHANGE_EMAIL, RESET_PASSWORD, CHANGE_PASSWORD, - CONFIRM_EMAIL + CONFIRM_EMAIL, + CONFIRM_PASSWORD, ) def __init__(self, purpose: int): @@ -320,7 +322,8 @@ class GMTokenGenerator(PasswordResetTokenGenerator): self.purpose == self.CONFIRM_EMAIL: fields.extend([str(user.email_confirmed), str(user.email)]) elif self.purpose == self.RESET_PASSWORD or \ - self.purpose == self.CHANGE_PASSWORD: + self.purpose == self.CHANGE_PASSWORD or \ + self.purpose == self.CONFIRM_PASSWORD: fields.append(str(user.password)) return fields diff --git a/project/settings/base.py b/project/settings/base.py index ae4b0201..1437dd9e 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -404,6 +404,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1 # TEMPLATES RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html' CHANGE_EMAIL_TEMPLATE = 'account/change_email.html' +CONFIRM_PASSWORD_TEMPLATE = 'account/password_confirm_email.html' CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html' NEWS_EMAIL_TEMPLATE = "news/news_email.html" diff --git a/project/templates/account/change_email.html b/project/templates/account/change_email.html index 0a257ed6..6b74d970 100644 --- a/project/templates/account/change_email.html +++ b/project/templates/account/change_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page for confirmation new email address:" %} -https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ +https://{{ country_code }}.{{ domain_uri }}/change-email-confirm/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} diff --git a/project/templates/account/password_confirm_email.html b/project/templates/account/password_confirm_email.html new file mode 100644 index 00000000..29f27afb --- /dev/null +++ b/project/templates/account/password_confirm_email.html @@ -0,0 +1,11 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}Confirm a password reset for your user account at {{ site_name }}.{% endblocktrans %} + +{% trans "Please go to the following page:" %} + +https://{{ country_code }}.{{ domain_uri }}/confirm-new-password/{{ uidb64 }}/{{ token }}/ + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +{% endautoescape %} \ No newline at end of file diff --git a/project/templates/news/news_email.html b/project/templates/news/news_email.html index b3782f5f..d14bd898 100644 --- a/project/templates/news/news_email.html +++ b/project/templates/news/news_email.html @@ -36,7 +36,7 @@
{{ description | safe }}
- +