From f8c795cd031e1923f1c619c9e97a8976b843403e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Sep 2019 18:53:02 +0300 Subject: [PATCH] added redirect for ConfirmEmail and ResetPassword --- apps/account/models.py | 16 +++++----- apps/account/serializers/web.py | 28 ++++++++++------- apps/account/tasks.py | 4 +-- apps/account/views/common.py | 26 ++++++++++++++++ apps/authorization/models.py | 15 +++++----- apps/authorization/serializers/common.py | 30 +++++++++++-------- apps/authorization/tasks.py | 4 +-- apps/authorization/views/common.py | 10 ++++++- apps/utils/tokens.py | 11 ++++--- project/settings/stage.py | 17 +++++++++++ .../account/password_reset_email.html | 2 +- .../authorization/confirm_email.html | 5 ++-- 12 files changed, 114 insertions(+), 54 deletions(-) create mode 100644 project/settings/stage.py diff --git a/apps/account/models.py b/apps/account/models.py index 508a52fd..30255802 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -94,9 +94,9 @@ class User(ImageMixin, AbstractUser): self.is_active = switcher self.save() - def create_jwt_tokens(self, source: int): + def create_jwt_tokens(self, source: int = None): """Create JWT tokens for user""" - token = GMRefreshToken.for_user_by_source(self, source) + token = GMRefreshToken.for_user(self, source) return { 'access_token': str(token.access_token), 'refresh_token': str(token), @@ -154,15 +154,15 @@ class User(ImageMixin, AbstractUser): """Get base64 value for user by primary key identifier""" return urlsafe_base64_encode(force_bytes(self.pk)) - @property - def confirm_email_template(self): + 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}) + 'site_name': settings.SITE_NAME, + 'country_code': country_code}) @property def change_email_template(self): @@ -245,15 +245,15 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin): """Generates a pseudo random code""" return password_token_generator.make_token(self.user) - @property - def reset_password_template(self): + def reset_password_template(self, country_code): """Get reset password 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}) + 'site_name': settings.SITE_NAME, + 'country_code': country_code}) @staticmethod def token_is_valid(user, token): diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 60a68820..cdf616dc 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -22,21 +22,27 @@ class PasswordResetSerializer(serializers.ModelSerializer): 'username_or_email', ) + @property + def request(self): + """Get request from context""" + return self.context.get('request') + def validate(self, attrs): """Override validate method""" - user = self.context.get('request').user + user = self.request.user if user.is_anonymous: username_or_email = attrs.get('username_or_email') if not username_or_email: raise serializers.ValidationError(_('Username or Email not in request body.')) # Check user in DB - username_or_email = (username_or_email.lower() - if username_validator(username_or_email) is False - else username_or_email) - user_qs = models.User.objects.filter(Q(email=username_or_email) | - Q(username=username_or_email)) - if user_qs.exists(): + filters = {} + if username_validator(username_or_email): + filters.update({'username': 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 @@ -45,16 +51,18 @@ class PasswordResetSerializer(serializers.ModelSerializer): def create(self, validated_data, *args, **kwargs): """Override create method""" user = validated_data.pop('user') - ip_address = self.context.get('request').META.get('REMOTE_ADDR') + 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(obj.id) + tasks.send_reset_password_email.delay(request_id=obj.id, + country_code=self.request.country_code) else: - tasks.send_reset_password_email(obj.id) + tasks.send_reset_password_email(request_id=obj.id, + country_code=self.request.country_code) return obj diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 0367a59e..362daddf 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -11,13 +11,13 @@ logger = logging.getLogger(__name__) @shared_task -def send_reset_password_email(request_id): +def send_reset_password_email(request_id, country_code): """Send email to user for reset password.""" try: obj = models.ResetPasswordToken.objects.get(id=request_id) user = obj.user user.send_email(subject=_('Password resetting'), - message=obj.reset_password_template) + message=obj.reset_password_template(country_code)) except: logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' f'DETAIL: Exception occurred for ResetPasswordToken instance: ' diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 2e182bd9..360a1e5a 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -6,6 +6,7 @@ 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 @@ -29,6 +30,31 @@ class ChangePasswordView(JWTUpdateAPIView): """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(): + queryset = self.filter_queryset(self.get_queryset()) + 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() + # todo: Add is_valid check status + 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""" diff --git a/apps/authorization/models.py b/apps/authorization/models.py index 69416420..c295329c 100644 --- a/apps/authorization/models.py +++ b/apps/authorization/models.py @@ -38,8 +38,8 @@ class Application(PlatformMixin, AbstractApplication): class JWTAccessTokenManager(models.Manager): """Manager for AccessToken model.""" - def add_to_db(self, user, access_token: AccessToken, - refresh_token: RefreshToken): + + def make(self, user, access_token: AccessToken, refresh_token: RefreshToken): """Create generated tokens to DB""" refresh_token_qs = JWTRefreshToken.objects.filter(user=user, jti=refresh_token.payload.get('jti')) @@ -106,18 +106,17 @@ class JWTAccessToken(ProjectBaseMixin): class JWTRefreshTokenManager(models.Manager): """Manager for model RefreshToken.""" - - def add_to_db(self, user, token: RefreshToken, source: int): - """Added generated refresh token to db""" + def make(self, user, token: RefreshToken, source: int): + """Make method""" jti = token[settings.SIMPLE_JWT.get('JTI_CLAIM')] exp = token['exp'] obj = self.model( user=user, jti=jti, - source=source, created_at=token.current_time, - expires_at=utils.datetime_from_epoch(exp), - ) + expires_at=utils.datetime_from_epoch(exp)) + if source: + obj.source = source obj.save() return obj diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index f5e24fc9..4d29887c 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -19,13 +19,9 @@ from utils.tokens import GMRefreshToken class SignupSerializer(serializers.ModelSerializer): """Signup serializer serializer mixin""" # REQUEST - username = serializers.CharField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True) + username = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True) - email = serializers.EmailField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True) + email = serializers.EmailField(write_only=True) newsletter = serializers.BooleanField(write_only=True) class Meta: @@ -42,6 +38,14 @@ class SignupSerializer(serializers.ModelSerializer): valid = utils_methods.username_validator(username=value) if not valid: raise utils_exceptions.NotValidUsernameError() + if account_models.User.objects.filter(username__icontains=value).exists(): + raise serializers.ValidationError() + return value + + def validate_email(self, value): + """Validate email""" + if account_models.User.objects.filter(email__icontains=value).exists(): + raise serializers.ValidationError() return value def validate_password(self, value): @@ -62,9 +66,13 @@ class SignupSerializer(serializers.ModelSerializer): newsletter=validated_data.get('newsletter')) # Send verification link on user email if settings.USE_CELERY: - tasks.send_confirm_email.delay(obj.id) + tasks.send_confirm_email.delay( + user_id=obj.id, + country_code=self.context.get('request').country_code) else: - tasks.send_confirm_email(obj.id) + tasks.send_confirm_email( + user_id=obj.id, + country_code=self.context.get('request').country_code) return obj @@ -128,14 +136,10 @@ class RefreshTokenSerializer(SourceSerializerMixin): refresh_token = serializers.CharField(read_only=True) access_token = serializers.CharField(read_only=True) - def get_request(self): - """Return request""" - return self.context.get('request') - def validate(self, attrs): """Override validate method""" - cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') + cookie_refresh_token = self.context.get('request').COOKIES.get('refresh_token') # Check if refresh_token in COOKIES if not cookie_refresh_token: raise utils_exceptions.NotValidRefreshTokenError() diff --git a/apps/authorization/tasks.py b/apps/authorization/tasks.py index a2ae4bb3..9947c2a3 100644 --- a/apps/authorization/tasks.py +++ b/apps/authorization/tasks.py @@ -10,12 +10,12 @@ logger = logging.getLogger(__name__) @shared_task -def send_confirm_email(user_id): +def send_confirm_email(user_id, country_code): """Send verification email to user.""" try: obj = account_models.User.objects.get(id=user_id) obj.send_email(subject=_('Email confirmation'), - message=obj.confirm_email_template) + message=obj.confirm_email_template(country_code)) except: logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index c77dd63e..827ff108 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -182,7 +182,15 @@ class VerifyEmailConfirmView(JWTGenericViewMixin): raise utils_exceptions.NotValidTokenError() # Approve email status user.confirm_email() - return Response(status=status.HTTP_200_OK) + response = 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) else: raise utils_exceptions.UserNotFoundError() diff --git a/apps/utils/tokens.py b/apps/utils/tokens.py index e686b42b..a236bfa3 100644 --- a/apps/utils/tokens.py +++ b/apps/utils/tokens.py @@ -33,12 +33,11 @@ class GMBlacklistMixin(BlacklistMixin): """ @classmethod - def for_user_by_source(cls, user, source: int): + def for_user(cls, user, source: int = None): """Create a refresh token.""" token = super().for_user(user) token['user'] = user.get_user_info() - # Create a record in DB - JWTRefreshToken.objects.add_to_db(user=user, token=token, source=source) + JWTRefreshToken.objects.make(user=user, token=token, source=source) return token @@ -70,7 +69,7 @@ class GMRefreshToken(GMBlacklistMixin, GMToken, RefreshToken): # Create a record in DB user = User.objects.get(id=self.payload.get('user_id')) - JWTAccessToken.objects.add_to_db(user=user, - access_token=access_token, - refresh_token=self) + JWTAccessToken.objects.make(user=user, + access_token=access_token, + refresh_token=self) return access_token diff --git a/project/settings/stage.py b/project/settings/stage.py new file mode 100644 index 00000000..998abaa6 --- /dev/null +++ b/project/settings/stage.py @@ -0,0 +1,17 @@ +"""Stage settings.""" +from .base import * + +ALLOWED_HOSTS = ['gm-stage.id-east.ru', '95.213.204.126'] + +SEND_SMS = False +SMS_CODE_SHOW = True +USE_CELERY = False + +SCHEMA_URI = 'https' +DEFAULT_SUBDOMAIN = 'www' +SITE_DOMAIN_URI = 'id-east.ru' +DOMAIN_URI = 'gm-stage.id-east.ru' + +# COOKIES +CSRF_COOKIE_DOMAIN = '.id-east.ru' +SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index 1a395cee..b743d71c 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -http://{{ domain_uri }}{% url 'web:account:form-password-reset-confirm' uidb64=uidb64 token=token %} +Reset link. {% endblock %} {% trans "Thanks for using our site!" %} diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 7fa06aa5..9056525d 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -2,9 +2,8 @@ {% blocktrans %}You're receiving this email because you trying to register new account at {{ site_name }}.{% endblocktrans %} {% trans "Please confirm your email address to complete the registration:" %} -{% block signup_confirm %} -http://{{ domain_uri }}{% url 'auth:signup-confirm' uidb64=uidb64 token=token %} -{% endblock %} + +Confirmation link. {% trans "Thanks for using our site!" %}