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 diff --git a/apps/account/admin.py b/apps/account/admin.py index 938be965..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', @@ -47,11 +49,14 @@ class UserAdmin(BaseUserAdmin): short_name.short_description = _('Name') + def image_preview(self, obj): + """Get user image preview""" + return obj.image_tag -@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', ) + 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/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/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 b0ae79d6..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 @@ -52,16 +51,13 @@ 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): +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) @@ -98,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), @@ -151,7 +147,7 @@ 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): @@ -159,112 +155,43 @@ class User(ImageMixin, AbstractUser): return urlsafe_base64_encode(force_bytes(self.pk)) @property - def confirm_email_template(self): - """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}) + def base_template(self): + """Base email template""" + return {'domain_uri': settings.DOMAIN_URI, + 'uidb64': self.get_user_uidb64, + 'site_name': settings.SITE_NAME} @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) + def image_tag(self): + return mark_safe(f'') @property - def get_resetting_token_expiration(self): - """Get resetting token expiration""" - return settings.RESETTING_TOKEN_EXPIRATION + def cropped_image_tag(self): + return mark_safe(f'') - @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) - - @property - def reset_password_template(self): + 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}) + 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 2f88d426..0c72d036 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,16 @@ 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()),), + 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) + 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) newsletter = serializers.BooleanField(required=False) class Meta: @@ -32,8 +38,8 @@ class UserSerializer(serializers.ModelSerializer): 'first_name', 'last_name', 'fullname', - 'cropped_image', - 'image', + 'cropped_image_url', + 'image_url', 'email', 'email_confirmed', 'newsletter', @@ -42,33 +48,31 @@ 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): - """Validate username""" - if models.User.objects.by_username(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): - 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 @@ -156,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 cee66bfa..d325cea6 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -1,61 +1,49 @@ """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""" + return self.context.get('request') def validate(self, attrs): """Override validate method""" - user = self.context.get('request').user + 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 requested')) - # Check user in DB - 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() + raise serializers.ValidationError(_('username or email not in request body.')) + + filters = {} + if username_validator(username_or_email): + filters.update({'username__icontains': username_or_email}) else: - raise utils_exceptions.UserNotFoundError() - 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.context.get('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) - else: - tasks.send_reset_password_email(obj.id) - return obj - class PasswordResetConfirmSerializer(serializers.ModelSerializer): """Serializer for model User""" @@ -64,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 0367a59e..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): +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) + 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 2e182bd9..ab62343f 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -11,8 +11,7 @@ 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 @@ -25,7 +24,7 @@ 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() @@ -39,13 +38,13 @@ class ChangePasswordView(JWTUpdateAPIView): 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 @@ -55,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,) @@ -68,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 af147ba6..e4596e9f 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -1,46 +1,43 @@ """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 (JWTCreateAPIView, - JWTGenericViewMixin) +from utils.views import JWTGenericViewMixin -class PasswordResetView(JWTCreateAPIView): +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'): + 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) 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 @@ -51,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/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 e5c615e0..5d8bb3a8 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -19,14 +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: @@ -38,34 +33,46 @@ 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 + if account_models.User.objects.filter(username__icontains=value).exists(): + raise serializers.ValidationError() + return value - def validate_password(self, data): + 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): """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""" 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: - 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 @@ -108,7 +115,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 @@ -129,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/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 c77dd63e..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, ) @@ -182,7 +181,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/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/establishment/models.py b/apps/establishment/models.py index a0e28d11..5a187f89 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/serializers/common.py b/apps/establishment/serializers/common.py index b8d212f8..d6b21180 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.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 @@ -148,7 +149,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') class Meta: """Meta class.""" @@ -338,3 +339,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..dc96c542 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -5,9 +5,9 @@ from establishment import views 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(), @@ -15,5 +15,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/apps/establishment/views/web.py b/apps/establishment/views/web.py index b1a8d8d4..354b12a0 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.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 from establishment.views import EstablishmentMixin @@ -108,3 +109,34 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D # 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) + + +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') diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py index 74748e94..80498e65 100644 --- a/apps/favorites/urls.py +++ b/apps/favorites/urls.py @@ -8,5 +8,5 @@ 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='remove-from-favorites'), ] 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, ) 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 b2730b97..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 @@ -137,8 +128,17 @@ 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.') + + +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/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/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/apps/utils/views.py b/apps/utils/views.py index d01d30cd..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""" @@ -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): @@ -77,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 fceaa632..16ac6002 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', @@ -87,6 +86,7 @@ EXTERNAL_APPS = [ 'rest_framework_simplejwt.token_blacklist', 'solo', 'phonenumber_field', + 'corsheaders', ] @@ -333,10 +333,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') @@ -373,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' @@ -381,6 +376,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/project/settings/stage.py b/project/settings/stage.py new file mode 100644 index 00000000..bbb2f245 --- /dev/null +++ b/project/settings/stage.py @@ -0,0 +1,13 @@ +"""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' 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 1a395cee..ee84fd0b 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -2,9 +2,8 @@ {% 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 %} -http://{{ domain_uri }}{% url 'web:account:form-password-reset-confirm' uidb64=uidb64 token=token %} -{% endblock %} + +https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 7fa06aa5..f3bbd50e 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 %} + +https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} 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 +]