From 500e470b39da3d91c72cfb9be86fd7b38e498bbc Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 15:01:10 +0300 Subject: [PATCH 01/21] Added CORSMiddleware --- apps/utils/middleware.py | 10 ++++++++++ project/settings/base.py | 7 +------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index 3cc57319..7203127c 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -32,3 +32,13 @@ def parse_cookies(get_response): return response return middleware + +class CORSMiddleware: + """Added parameter {Access-Control-Allow-Origin: *} to response""" + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response["Access-Control-Allow-Origin"] = '*' + return response diff --git a/project/settings/base.py b/project/settings/base.py index fceaa632..4e8a7acc 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -79,7 +79,6 @@ EXTERNAL_APPS = [ 'rest_framework', 'rest_framework.authtoken', 'easy_select2', - 'corsheaders', 'oauth2_provider', 'social_django', 'rest_framework_social_oauth2', @@ -98,13 +97,13 @@ MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'oauth2_provider.middleware.OAuth2TokenMiddleware', - 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware.parse_cookies', + 'utils.middleware.CORSMiddleware', ] ROOT_URLCONF = 'project.urls' @@ -333,10 +332,6 @@ THUMBNAIL_ALIASES = { # Password reset RESETTING_TOKEN_EXPIRATION = 24 # hours -# CORS Config -CORS_ORIGIN_ALLOW_ALL = True -CORS_ALLOW_CREDENTIALS = True - GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db') From c04d7b1bd3eb852e18d39006f94b878d8fdc6200 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 15:01:45 +0300 Subject: [PATCH 02/21] remove unused dependencies --- requirements/base.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 1c70e7c2..20773192 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,8 +24,5 @@ django-rest-framework-social-oauth2==1.1.0 django-extensions==2.2.1 -# CORS -django-cors-headers==3.0.2 - # JWT djangorestframework-simplejwt==4.3.0 \ No newline at end of file From cf1c10d6ecf62d6971e10417b815ef52f4ad7b97 Mon Sep 17 00:00:00 2001 From: "a.feteleu" Date: Tue, 10 Sep 2019 12:19:00 +0000 Subject: [PATCH 03/21] Revert "small refactoring favorites" This reverts commit a4d7430379cd10d1d7ac9b2c51b7d2660b2172fe --- apps/favorites/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py index 74748e94..4b8e8088 100644 --- a/apps/favorites/urls.py +++ b/apps/favorites/urls.py @@ -1,12 +1,12 @@ """Favorites urlpaths.""" from django.urls import path - from . import views + app_name = 'favorites' urlpatterns = [ path('establishments/', views.FavoritesEstablishmentListView.as_view(), name='establishment-list'), - path('/', views.FavoritesDestroyView.as_view(), name='remove-from-favorites'), + path('remove//', views.FavoritesDestroyView.as_view(), name='delete-favorites'), ] From 7e468c539abcfb62b91496f148db69356196e053 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 15:35:24 +0300 Subject: [PATCH 04/21] change project settings --- apps/favorites/urls.py | 4 ++-- project/settings/base.py | 10 +++++++++- requirements/base.txt | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py index 4b8e8088..80498e65 100644 --- a/apps/favorites/urls.py +++ b/apps/favorites/urls.py @@ -1,12 +1,12 @@ """Favorites urlpaths.""" from django.urls import path -from . import views +from . import views app_name = 'favorites' urlpatterns = [ path('establishments/', views.FavoritesEstablishmentListView.as_view(), name='establishment-list'), - path('remove//', views.FavoritesDestroyView.as_view(), name='delete-favorites'), + path('remove//', views.FavoritesDestroyView.as_view(), name='remove-from-favorites'), ] diff --git a/project/settings/base.py b/project/settings/base.py index 4e8a7acc..995391c2 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -86,6 +86,7 @@ EXTERNAL_APPS = [ 'rest_framework_simplejwt.token_blacklist', 'solo', 'phonenumber_field', + 'corsheaders', ] @@ -93,6 +94,7 @@ INSTALLED_APPS = CONTRIB_APPS + PROJECT_APPS + EXTERNAL_APPS MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -103,7 +105,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware.parse_cookies', - 'utils.middleware.CORSMiddleware', + # 'utils.middleware.CORSMiddleware', ] ROOT_URLCONF = 'project.urls' @@ -376,6 +378,12 @@ CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html' # COOKIES COOKIES_MAX_AGE = 86400 # 24 hours +SESSION_COOKIE_SAMESITE = None + + +# CORS +CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_CREDENTIALS = True # UPLOAD FILES diff --git a/requirements/base.txt b/requirements/base.txt index 20773192..1c70e7c2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,5 +24,8 @@ django-rest-framework-social-oauth2==1.1.0 django-extensions==2.2.1 +# CORS +django-cors-headers==3.0.2 + # JWT djangorestframework-simplejwt==4.3.0 \ No newline at end of file From 8e506bd5e1240089fd111738717bb54489af085d Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:05:41 +0300 Subject: [PATCH 05/21] added cookie settings for local and development --- project/settings/development.py | 6 +++++- project/settings/local.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/project/settings/development.py b/project/settings/development.py index 7ecc6aa2..2bf4d952 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,5 +1,4 @@ """Development settings.""" -from .base import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126'] @@ -11,3 +10,8 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' + + +# COOKIES +CSRF_COOKIE_DOMAIN = '.id-east.ru' +SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index 1c73a857..6edf7124 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -12,6 +12,12 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' + +# COOKIES +CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' +SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' + + # CELERY BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL From b5707c6b26cf387e7e16fa91767b3424cf54fdbf Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:12:56 +0300 Subject: [PATCH 06/21] Revert "added cookie settings for local and development" This reverts commit 8e506bd5 --- project/settings/development.py | 5 ----- project/settings/local.py | 6 ------ 2 files changed, 11 deletions(-) diff --git a/project/settings/development.py b/project/settings/development.py index 2bf4d952..99bfcc01 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -10,8 +10,3 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' - - -# COOKIES -CSRF_COOKIE_DOMAIN = '.id-east.ru' -SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index 6edf7124..1c73a857 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -12,12 +12,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' - -# COOKIES -CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' -SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' - - # CELERY BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL From 2d102f0005e40479261246bd9ba6c25310d148fb Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:16:03 +0300 Subject: [PATCH 07/21] added cookie settings for local and development --- project/settings/development.py | 5 +++++ project/settings/local.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/project/settings/development.py b/project/settings/development.py index 99bfcc01..f37a40b4 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,4 +1,5 @@ """Development settings.""" +from .base import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126'] @@ -10,3 +11,7 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' + +# COOKIES +CSRF_COOKIE_DOMAIN = '.id-east.ru' +SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index 1c73a857..b40e8bef 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -17,6 +17,10 @@ BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL +# COOKIES +CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' +SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' + # LOGGING LOGGING = { From 530ce3852780ec667096de6a014133e19469f0f0 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:53:50 +0300 Subject: [PATCH 08/21] fixed account detail --- apps/account/models.py | 4 ---- apps/account/serializers/common.py | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index b0ae79d6..508a52fd 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -52,10 +52,6 @@ class UserQuerySet(models.QuerySet): return self.filter(oauth2_provider_refreshtoken__token=token, oauth2_provider_refreshtoken__expires__gt=timezone.now()) - def by_username(self, username: str): - """Filter users by username.""" - return self.filter(username=username) - class User(ImageMixin, AbstractUser): """Base user model.""" diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 2f88d426..deb0217e 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -43,11 +43,13 @@ class UserSerializer(serializers.ModelSerializer): """Validate email value""" if value == self.instance.email: raise serializers.ValidationError() + if models.User.objects.filter(email=value).exists(): + raise serializers.ValidationError() return value def validate_username(self, value): """Validate username""" - if models.User.objects.by_username(username=value).exists(): + if models.User.objects.filter(username=value).exists(): raise serializers.ValidationError() return value From 434df203da2f88ca1b69e02b84c173d27d7fe1ed Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 18:03:01 +0300 Subject: [PATCH 09/21] fixed account detail --- apps/account/serializers/common.py | 20 +++++++++++++------- apps/authorization/serializers/common.py | 15 +++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index deb0217e..b6de5bb0 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -4,9 +4,11 @@ from django.contrib.auth import password_validation as password_validators from fcm_django.models import FCMDevice from rest_framework import exceptions from rest_framework import serializers +from rest_framework import validators as rest_validators from account import models, tasks from utils import exceptions as utils_exceptions +from utils import methods as utils_methods # User serializers @@ -17,12 +19,17 @@ class UserSerializer(serializers.ModelSerializer): fullname = serializers.CharField(source='get_full_name', read_only=True) # REQUEST - username = serializers.CharField(required=False) + username = serializers.CharField( + validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), + write_only=True, + required=False) first_name = serializers.CharField(required=False, write_only=True) last_name = serializers.CharField(required=False, write_only=True) image = serializers.ImageField(required=False) cropped_image = serializers.ImageField(required=False) - email = serializers.EmailField(required=False) + email = serializers.EmailField( + validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), + required=False) newsletter = serializers.BooleanField(required=False) class Meta: @@ -43,14 +50,13 @@ class UserSerializer(serializers.ModelSerializer): """Validate email value""" if value == self.instance.email: raise serializers.ValidationError() - if models.User.objects.filter(email=value).exists(): - raise serializers.ValidationError() return value def validate_username(self, value): - """Validate username""" - if models.User.objects.filter(username=value).exists(): - raise serializers.ValidationError() + """Custom username validation""" + valid = utils_methods.username_validator(username=value) + if not valid: + raise utils_exceptions.NotValidUsernameError() return value def validate(self, attrs): diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index e5c615e0..5817e191 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -21,8 +21,7 @@ class SignupSerializer(serializers.ModelSerializer): # REQUEST username = serializers.CharField( validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True - ) + write_only=True) password = serializers.CharField(write_only=True) email = serializers.EmailField( validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), @@ -38,21 +37,21 @@ class SignupSerializer(serializers.ModelSerializer): 'newsletter' ) - def validate_username(self, data): + def validate_username(self, value): """Custom username validation""" - valid = utils_methods.username_validator(username=data) + valid = utils_methods.username_validator(username=value) if not valid: raise utils_exceptions.NotValidUsernameError() - return data + return value - def validate_password(self, data): + def validate_password(self, value): """Custom password validation""" try: - password_validators.validate_password(password=data) + password_validators.validate_password(password=value) except serializers.ValidationError as e: raise serializers.ValidationError(str(e)) else: - return data + return value def create(self, validated_data): """Override create method""" From cf884ed906ccef57dc0484c98923b5fe41e345a3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Sep 2019 12:07:30 +0300 Subject: [PATCH 10/21] fixed UserSerializer, PasswordResetSerializer --- apps/account/serializers/common.py | 1 - apps/account/serializers/web.py | 11 +++--- apps/account/views/web.py | 13 +++++-- apps/authorization/serializers/common.py | 2 +- apps/utils/views.py | 46 ++++++++++++++++-------- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index b6de5bb0..bd28cb88 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -21,7 +21,6 @@ class UserSerializer(serializers.ModelSerializer): # REQUEST username = serializers.CharField( validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), - write_only=True, required=False) first_name = serializers.CharField(required=False, write_only=True) last_name = serializers.CharField(required=False, write_only=True) diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index cee66bfa..60a68820 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -7,6 +7,7 @@ from rest_framework import serializers from account import models, tasks from utils import exceptions as utils_exceptions +from utils.methods import username_validator class PasswordResetSerializer(serializers.ModelSerializer): @@ -28,14 +29,15 @@ class PasswordResetSerializer(serializers.ModelSerializer): if user.is_anonymous: username_or_email = attrs.get('username_or_email') if not username_or_email: - raise serializers.ValidationError(_('Username or Email not requested')) + 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(): attrs['user'] = user_qs.first() - else: - raise utils_exceptions.UserNotFoundError() else: attrs['user'] = user return attrs @@ -48,8 +50,7 @@ class PasswordResetSerializer(serializers.ModelSerializer): obj = models.ResetPasswordToken.objects.create( user=user, ip_address=ip_address, - source=models.ResetPasswordToken.WEB - ) + source=models.ResetPasswordToken.WEB) if settings.USE_CELERY: tasks.send_reset_password_email.delay(obj.id) else: diff --git a/apps/account/views/web.py b/apps/account/views/web.py index af147ba6..4f10ccc2 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -22,16 +22,23 @@ from account.forms import SetPasswordForm from account.serializers import web as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator -from utils.views import (JWTCreateAPIView, - JWTGenericViewMixin) +from utils.views import JWTGenericViewMixin -class PasswordResetView(JWTCreateAPIView): +class PasswordResetView(JWTGenericViewMixin): """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() + return Response(status=status.HTTP_200_OK) + class PasswordResetConfirmView(JWTGenericViewMixin): """View for confirmation new password""" diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 5817e191..f5e24fc9 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -58,7 +58,7 @@ class SignupSerializer(serializers.ModelSerializer): obj = account_models.User.objects.make( username=validated_data.get('username'), password=validated_data.get('password'), - email=validated_data.get('email'), + email=validated_data.get('email').lower(), newsletter=validated_data.get('newsletter')) # Send verification link on user email if settings.USE_CELERY: diff --git a/apps/utils/views.py b/apps/utils/views.py index d01d30cd..d129fb73 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -16,6 +16,13 @@ class JWTGenericViewMixin(generics.GenericAPIView): REFRESH_TOKEN_HTTP_ONLY = False REFRESH_TOKEN_SECURE = False + + LOCALE_HTTP_ONLY = False + LOCALE_SECURE = False + + COUNTRY_CODE_HTTP_ONLY = False + COUNTRY_CODE_SECURE = False + COOKIE = namedtuple('COOKIE', ['key', 'value', 'http_only', 'secure', 'max_age']) def _put_data_in_cookies(self, @@ -26,21 +33,32 @@ class JWTGenericViewMixin(generics.GenericAPIView): cookies it is list that contain namedtuples cookies would contain key, value and secure parameters. """ - COOKIES = list() + COOKIES = [] - # Write to cookie access and refresh token with secure flag - if access_token and refresh_token: - _access_token = self.COOKIE(key='access_token', - value=access_token, - http_only=self.ACCESS_TOKEN_HTTP_ONLY, - secure=self.ACCESS_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None) - _refresh_token = self.COOKIE(key='refresh_token', - value=refresh_token, - http_only=self.REFRESH_TOKEN_HTTP_ONLY, - secure=self.REFRESH_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None) - COOKIES.extend((_access_token, _refresh_token)) + if hasattr(self.request, 'locale'): + COOKIES.append(self.COOKIE(key='locale', + value=self.request.locale, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.LOCALE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if hasattr(self.request, 'country_code'): + COOKIES.append(self.COOKIE(key='country_code', + value=self.request.country_code, + http_only=self.COUNTRY_CODE_HTTP_ONLY, + secure=self.COUNTRY_CODE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if access_token: + COOKIES.append(self.COOKIE(key='access_token', + value=access_token, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.ACCESS_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if refresh_token: + COOKIES.append(self.COOKIE(key='refresh_token', + value=refresh_token, + http_only=self.REFRESH_TOKEN_HTTP_ONLY, + secure=self.REFRESH_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) return COOKIES def _put_cookies_in_response(self, cookies: list, response: Response): From 2622395006871b24aa5136fb0672917eeb4b56ef Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 11 Sep 2019 17:06:01 +0300 Subject: [PATCH 11/21] add local docker conf override to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4aaea0c1..a32ff3df 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ logs/ /datadir/ /_files/ /geoip_db/ + +# dev +./docker-compose.override.yml \ No newline at end of file From f8c795cd031e1923f1c619c9e97a8976b843403e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Sep 2019 18:53:02 +0300 Subject: [PATCH 12/21] 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!" %} From 06c7d790bb4cf6529405b7909158793ebaa1ddd3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Sep 2019 18:53:02 +0300 Subject: [PATCH 13/21] 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!" %} From 2c41e532c8e81f0cf57a9493530ca934b69687bf Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 09:33:09 +0300 Subject: [PATCH 14/21] fixed ChangePasswordView --- apps/account/views/common.py | 9 +++++---- apps/utils/exceptions.py | 4 ++-- project/templates/account/password_reset_email.html | 5 +---- project/templates/authorization/confirm_email.html | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 360a1e5a..66ad3fa0 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -34,8 +34,7 @@ class ChangePasswordView(JWTUpdateAPIView): def get_object(self): """Overridden get_object method.""" - if not self.request.user.is_authenticated(): - queryset = self.filter_queryset(self.get_queryset()) + if not self.request.user.is_authenticated: uidb64 = self.kwargs.get('uidb64') user_id = force_text(urlsafe_base64_decode(uidb64)) @@ -47,7 +46,8 @@ class ChangePasswordView(JWTUpdateAPIView): 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 + if not password_reset_obj.user.is_active: + raise utils_exceptions.UserNotFoundError() obj = password_reset_obj.user else: obj = self.request.user @@ -58,7 +58,8 @@ class ChangePasswordView(JWTUpdateAPIView): def patch(self, request, *args, **kwargs): """Implement PUT method""" - serializer = self.get_serializer(instance=self.request.user, + instance = self.get_object() + serializer = self.get_serializer(instance=instance, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index b2730b97..5251c4e4 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -137,8 +137,8 @@ class WrongAuthCredentials(AuthErrorMixin): class FavoritesError(exceptions.APIException): """ - The exception should be thrown when you item that user - want add to favorites already exists. + The exception should be thrown when item that user + want to add to favorites is already exists. """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Item is already in favorites.') diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index b743d71c..c32469f7 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -2,10 +2,7 @@ {% 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:" %} -{% block reset_link %} -Reset link. -{% endblock %} - +https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %} diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 9056525d..f3bbd50e 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -3,7 +3,7 @@ {% trans "Please confirm your email address to complete the registration:" %} -Confirmation link. +https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} From ec2e87fe1a099e0c48186c09e84bbfe5e61b8ac1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 10:20:11 +0300 Subject: [PATCH 15/21] fixed settings for COOKIES, related to unable to login in admin page (CSRF ERROR) --- project/settings/base.py | 3 +-- project/settings/development.py | 4 ---- project/settings/local.py | 4 ---- project/settings/stage.py | 4 ---- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/project/settings/base.py b/project/settings/base.py index 995391c2..b7047f98 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -94,18 +94,17 @@ INSTALLED_APPS = CONTRIB_APPS + PROJECT_APPS + EXTERNAL_APPS MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'oauth2_provider.middleware.OAuth2TokenMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware.parse_cookies', - # 'utils.middleware.CORSMiddleware', ] ROOT_URLCONF = 'project.urls' diff --git a/project/settings/development.py b/project/settings/development.py index f37a40b4..7ecc6aa2 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -11,7 +11,3 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' - -# COOKIES -CSRF_COOKIE_DOMAIN = '.id-east.ru' -SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index b40e8bef..1c73a857 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -17,10 +17,6 @@ BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL -# COOKIES -CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' -SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' - # LOGGING LOGGING = { diff --git a/project/settings/stage.py b/project/settings/stage.py index 998abaa6..bbb2f245 100644 --- a/project/settings/stage.py +++ b/project/settings/stage.py @@ -11,7 +11,3 @@ 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' From 5943839edacf569a147f1064fbe4f396240aeafb Mon Sep 17 00:00:00 2001 From: Semyon Date: Thu, 12 Sep 2019 14:24:48 +0300 Subject: [PATCH 16/21] Added view for nearest establishments --- apps/establishment/models.py | 14 ++++++++++++++ apps/establishment/views.py | 25 ++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index efe665ef..a0c9c44c 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -102,6 +102,20 @@ class EstablishmentQuerySet(models.QuerySet): default=False, output_field=models.BooleanField(default=False))) + def by_distance_from_point(self, center, radius, unit='m'): + """ + Returns nearest establishments + + :param center: point from which to find nearby establishments + :param radius: the maximum distance within the radius of which to look for establishments + :return: all establishments within the specified radius of specified point + :param unit: length unit e.g. m, km. Default is 'm'. + """ + + from django.contrib.gis.measure import Distance + kwargs = {unit: radius} + return self.filter(address__coordinates__distance_lte=(center, Distance(**kwargs))) + class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin): """Establishment model.""" diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 528d2377..7745af2d 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -111,9 +111,28 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D obj = get_object_or_404( self.request.user.favorites.by_user(user=self.request.user) - .by_content_type(app_label='establishment', - model='establishment') - .by_object_id(object_id=self.kwargs['pk'])) + .by_content_type(app_label='establishment', + model='establishment') + .by_object_id(object_id=self.kwargs['pk'])) # May raise a permission denied self.check_object_permissions(self.request, obj) return obj + + +class EstablishmentNearestRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView): + """Resource for getting list of nearest establishments.""" + serializer_class = serializers.EstablishmentListSerializer + filter_class = filters.EstablishmentFilter + + def get_queryset(self): + """Overrided method 'get_queryset'.""" + from django.contrib.gis.geos import Point + + center = Point(float(self.request.query_params["lat"]), float(self.request.query_params["lon"])) + radius = float(self.request.query_params["radius"]) + unit = self.request.query_params.get("unit", None) + by_distance_from_point_kwargs = {"center": center, "radius": radius, "unit": unit} + return super(EstablishmentNearestRetrieveView, self).get_queryset() \ + .by_distance_from_point(**{k: v for k, v in by_distance_from_point_kwargs.items() if v is not None}) \ + .by_country_code(code=self.request.country_code) \ + .annotate_in_favorites(user=self.request.user) From bdc2c327f6fe4d3fe4d0ae827e4d063be076892b Mon Sep 17 00:00:00 2001 From: Semyon Date: Thu, 12 Sep 2019 14:25:03 +0300 Subject: [PATCH 17/21] Registered path for nearest establishments --- apps/establishment/urls/common.py | 5 ++--- apps/establishment/urls/mobile.py | 11 +++++++++++ project/urls/mobile.py | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 apps/establishment/urls/mobile.py diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 0f2958eb..ff6af5c8 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -5,7 +5,6 @@ from establishment import views app_name = 'establishment' - urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), @@ -15,5 +14,5 @@ urlpatterns = [ path('/comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), path('/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), - name='add-favorites'), -] \ No newline at end of file + name='add-favorites') +] diff --git a/apps/establishment/urls/mobile.py b/apps/establishment/urls/mobile.py new file mode 100644 index 00000000..2803be18 --- /dev/null +++ b/apps/establishment/urls/mobile.py @@ -0,0 +1,11 @@ +"""Establishment url patterns.""" +from django.urls import path + +from establishment import views +from establishment.urls.common import urlpatterns as common_urlpatterns + +urlpatterns = [ + path('geo/', views.EstablishmentNearestRetrieveView.as_view(), name='nearest-establishments-list') +] + +urlpatterns.extend(common_urlpatterns) diff --git a/project/urls/mobile.py b/project/urls/mobile.py index c007e260..0bcbd31c 100644 --- a/project/urls/mobile.py +++ b/project/urls/mobile.py @@ -3,10 +3,11 @@ from django.urls import path, include app_name = 'mobile' urlpatterns = [ + path('establishments/', include('establishment.urls.mobile')), # path('account/', include('account.urls.web')), # path('advertisement/', include('advertisement.urls.web')), # path('collection/', include('collection.urls.web')), # path('establishments/', include('establishment.urls.web')), # path('news/', include('news.urls.web')), # path('partner/', include('partner.urls.web')), -] \ No newline at end of file +] From 075298f6ec31ffacfc19ade212c58e41433f4055 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 14:44:49 +0300 Subject: [PATCH 18/21] refactored apps account, authorization, news, utils --- apps/account/admin.py | 9 - .../0006_delete_resetpasswordtoken.py | 16 ++ apps/account/models.py | 129 +++----------- apps/account/serializers/common.py | 33 ++-- apps/account/serializers/web.py | 79 +++------ apps/account/tasks.py | 27 ++- apps/account/urls/common.py | 4 +- apps/account/urls/web.py | 6 +- apps/account/views/common.py | 50 ++---- apps/account/views/web.py | 159 +++--------------- apps/authorization/urls/common.py | 2 +- apps/authorization/views/common.py | 9 +- apps/news/views/common.py | 5 +- apps/utils/exceptions.py | 18 +- apps/utils/views.py | 101 +---------- project/settings/base.py | 1 - project/templates/account/change_email.html | 5 +- .../account/password_reset_confirm.html | 31 ---- .../account/password_reset_email.html | 2 + 19 files changed, 165 insertions(+), 521 deletions(-) create mode 100644 apps/account/migrations/0006_delete_resetpasswordtoken.py delete mode 100644 project/templates/account/password_reset_confirm.html 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 %} From 1ee21edb8e0feb7b979d0e93eaac8040d69788a5 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 16:18:42 +0300 Subject: [PATCH 19/21] added endpoint to retrieve establishment tags --- apps/authorization/serializers/common.py | 2 +- apps/establishment/serializers.py | 21 +++++++++++++++------ apps/establishment/urls/common.py | 1 + apps/establishment/views.py | 13 +++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index f5e24fc9..a4c6f817 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -107,7 +107,7 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin, authentication = authenticate(username=user.get_username(), password=password) if not authentication: - raise utils_exceptions.WrongAuthCredentials() + raise utils_exceptions.UserNotFoundError() self.instance = user return attrs diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index c297ce54..3c1e3407 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -6,6 +6,7 @@ from comment.serializers import common as comment_serializers from establishment import models from favorites.models import Favorites from location.serializers import AddressSerializer +from main.models import MetaDataContent from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer from review import models as review_models from timetable.models import Timetable @@ -132,7 +133,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): subtypes = EstablishmentSubTypeSerializer(many=True) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) - preview_image = serializers.SerializerMethodField() + preview_image = serializers.ImageField(source='image', use_url=False) class Meta: """Meta class.""" @@ -151,11 +152,6 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): 'tags', ] - def get_preview_image(self, obj): - """Get preview image""" - return obj.get_full_image_url(request=self.context.get('request'), - thumbnail_key='establishment_preview') - class EstablishmentListSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" @@ -322,3 +318,16 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): 'content_object': validated_data.pop('establishment') }) return super().create(validated_data) + + +class EstablishmentTagListSerializer(serializers.ModelSerializer): + """List establishment tag serializer.""" + label_translated = serializers.CharField( + source='metadata.label_translated', read_only=True, allow_null=True) + + class Meta: + """Meta class.""" + model = MetaDataContent + fields = [ + 'label_translated', + ] diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 0f2958eb..007f7802 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -8,6 +8,7 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), + path('tags/', views.EstablishmentTagListView.as_view(), name='tags'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), path('/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), path('/comments/create/', views.EstablishmentCommentCreateView.as_view(), diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 528d2377..379407bd 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -6,6 +6,7 @@ from rest_framework import generics, permissions from comment import models as comment_models from establishment import filters from establishment import models, serializers +from main.models import MetaDataContent from utils.views import JWTGenericViewMixin @@ -117,3 +118,15 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D # May raise a permission denied self.check_object_permissions(self.request, obj) return obj + + +class EstablishmentTagListView(generics.ListAPIView): + """List view for establishment tags.""" + serializer_class = serializers.EstablishmentTagListSerializer + permission_classes = (permissions.AllowAny,) + pagination_class = None + + def get_queryset(self): + """Override get_queryset method""" + return MetaDataContent.objects.by_content_type(app_label='establishment', + model='establishment') From f45ac7e7fed84ae3b64313211dc35d8fa2aadaed Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 17:33:55 +0300 Subject: [PATCH 20/21] fixed establishment detail --- apps/establishment/serializers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 3c1e3407..abb3585a 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -133,7 +133,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): subtypes = EstablishmentSubTypeSerializer(many=True) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) - preview_image = serializers.ImageField(source='image', use_url=False) + preview_image = serializers.ImageField(source='image') class Meta: """Meta class.""" @@ -152,6 +152,11 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): 'tags', ] + def get_preview_image(self, obj): + """Get preview image""" + return obj.get_full_image_url(request=self.context.get('request'), + thumbnail_key='establishment_preview') + class EstablishmentListSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" From 1e650b9b1f33a3d93e05429651a8789fcefd5a4f Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 18:10:52 +0300 Subject: [PATCH 21/21] replace ImageFields to URLFields in User model, refactored serializers that included this fields --- apps/account/admin.py | 18 +++++++++++++-- .../migrations/0007_auto_20190912_1323.py | 21 +++++++++++++++++ .../migrations/0008_auto_20190912_1325.py | 23 +++++++++++++++++++ apps/account/models.py | 20 +++++++++++----- apps/account/serializers/common.py | 8 +++---- apps/comment/serializers/common.py | 7 ++---- apps/gallery/views.py | 2 +- 7 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 apps/account/migrations/0007_auto_20190912_1323.py create mode 100644 apps/account/migrations/0008_auto_20190912_1325.py diff --git a/apps/account/admin.py b/apps/account/admin.py index dc88c34b..8429952f 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -15,11 +15,13 @@ class UserAdmin(BaseUserAdmin): list_filter = ('is_active', 'is_staff', 'is_superuser', 'email_confirmed', 'groups',) search_fields = ('email', 'first_name', 'last_name') - readonly_fields = ('last_login', 'date_joined',) + readonly_fields = ('last_login', 'date_joined', 'image_preview', 'cropped_image_preview') fieldsets = ( (None, {'fields': ('email', 'password',)}), (_('Personal info'), { - 'fields': ('username', 'first_name', 'last_name', 'image')}), + 'fields': ('username', 'first_name', 'last_name', + 'image_url', 'image_preview', + 'cropped_image_url', 'cropped_image_preview',)}), (_('Subscription'), { 'fields': ( 'newsletter', @@ -46,3 +48,15 @@ class UserAdmin(BaseUserAdmin): return obj.get_short_name() short_name.short_description = _('Name') + + def image_preview(self, obj): + """Get user image preview""" + return obj.image_tag + + image_preview.short_description = 'Image preview' + + def cropped_image_preview(self, obj): + """Get user cropped image preview""" + return obj.cropped_image_tag + + cropped_image_preview.short_description = 'Cropped image preview' diff --git a/apps/account/migrations/0007_auto_20190912_1323.py b/apps/account/migrations/0007_auto_20190912_1323.py new file mode 100644 index 00000000..3fe08c7b --- /dev/null +++ b/apps/account/migrations/0007_auto_20190912_1323.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.4 on 2019-09-12 13:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0006_delete_resetpasswordtoken'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='cropped_image', + ), + migrations.RemoveField( + model_name='user', + name='image', + ), + ] diff --git a/apps/account/migrations/0008_auto_20190912_1325.py b/apps/account/migrations/0008_auto_20190912_1325.py new file mode 100644 index 00000000..b0a09ad2 --- /dev/null +++ b/apps/account/migrations/0008_auto_20190912_1325.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-09-12 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0007_auto_20190912_1323'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='cropped_image_url', + field=models.URLField(blank=True, default=None, null=True, verbose_name='Cropped image URL path'), + ), + migrations.AddField( + model_name='user', + name='image_url', + field=models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index ca5a4b96..81ade4fc 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -7,13 +7,12 @@ from django.db import models from django.template.loader import render_to_string from django.utils import timezone from django.utils.encoding import force_bytes +from django.utils.html import mark_safe from django.utils.http import urlsafe_base64_encode from django.utils.translation import ugettext_lazy as _ -from easy_thumbnails.fields import ThumbnailerImageField from rest_framework.authtoken.models import Token from authorization.models import Application -from utils.methods import image_path from utils.models import GMTokenGenerator from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin from utils.tokens import GMRefreshToken @@ -53,11 +52,12 @@ class UserQuerySet(models.QuerySet): oauth2_provider_refreshtoken__expires__gt=timezone.now()) -class User(ImageMixin, AbstractUser): +class User(AbstractUser): """Base user model.""" - cropped_image = ThumbnailerImageField(upload_to=image_path, - blank=True, null=True, default=None, - verbose_name=_('Crop image')) + image_url = models.URLField(verbose_name=_('Image URL path'), + blank=True, null=True, default=None) + cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'), + blank=True, null=True, default=None) email = models.EmailField(_('email address'), blank=True, null=True, default=None) email_confirmed = models.BooleanField(_('email status'), default=False) @@ -161,6 +161,14 @@ class User(ImageMixin, AbstractUser): 'uidb64': self.get_user_uidb64, 'site_name': settings.SITE_NAME} + @property + def image_tag(self): + return mark_safe(f'') + + @property + def cropped_image_tag(self): + return mark_safe(f'') + def reset_password_template(self, country_code): """Get reset password template""" context = {'token': self.reset_password_token, diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index e6599f96..0c72d036 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -24,8 +24,8 @@ class UserSerializer(serializers.ModelSerializer): required=False) first_name = serializers.CharField(required=False, write_only=True) last_name = serializers.CharField(required=False, write_only=True) - image = serializers.ImageField(required=False) - cropped_image = serializers.ImageField(required=False) + image_url = serializers.URLField(required=False) + cropped_image_url = serializers.URLField(required=False) email = serializers.EmailField( validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), required=False) @@ -38,8 +38,8 @@ class UserSerializer(serializers.ModelSerializer): 'first_name', 'last_name', 'fullname', - 'cropped_image', - 'image', + 'cropped_image_url', + 'image_url', 'email', 'email_confirmed', 'newsletter', diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py index 3598be4c..8175df7f 100644 --- a/apps/comment/serializers/common.py +++ b/apps/comment/serializers/common.py @@ -9,7 +9,8 @@ class CommentSerializer(serializers.ModelSerializer): nickname = serializers.CharField(read_only=True, source='user.username') is_mine = serializers.BooleanField(read_only=True) - profile_pic = serializers.SerializerMethodField() + profile_pic = serializers.URLField(read_only=True, + source='user.cropped_image_url') class Meta: """Serializer for model Comment""" @@ -24,7 +25,3 @@ class CommentSerializer(serializers.ModelSerializer): 'nickname', 'profile_pic' ] - - def get_profile_pic(self, obj): - """Get profile picture URL""" - return obj.user.get_full_image_url(request=self.context.get('request')) diff --git a/apps/gallery/views.py b/apps/gallery/views.py index a3b61727..109a01ef 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -6,7 +6,7 @@ from . import models, serializers class ImageUploadView(generics.CreateAPIView): """Upload image to gallery""" - permission_classes = (IsAuthenticatedAndTokenIsValid, ) model = models.Image queryset = models.Image.objects.all() serializer_class = serializers.ImageSerializer + permission_classes = (IsAuthenticatedAndTokenIsValid, )