refactored apps account, authorization, news, utils

This commit is contained in:
Anatoly 2019-09-12 14:44:49 +03:00
parent ec2e87fe1a
commit 075298f6ec
19 changed files with 165 additions and 521 deletions

View File

@ -46,12 +46,3 @@ class UserAdmin(BaseUserAdmin):
return obj.get_short_name() return obj.get_short_name()
short_name.short_description = _('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', )

View File

@ -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',
),
]

View File

@ -147,120 +147,43 @@ class User(ImageMixin, AbstractUser):
@property @property
def reset_password_token(self): def reset_password_token(self):
"""Make a token for finish signup.""" """Make a token for finish signup."""
return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self) return password_token_generator.make_token(self)
@property @property
def get_user_uidb64(self): def get_user_uidb64(self):
"""Get base64 value for user by primary key identifier""" """Get base64 value for user by primary key identifier"""
return urlsafe_base64_encode(force_bytes(self.pk)) 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 @property
def change_email_template(self): def base_template(self):
"""Get change email template""" """Base email template"""
return render_to_string( return {'domain_uri': settings.DOMAIN_URI,
template_name=settings.CHANGE_EMAIL_TEMPLATE, 'uidb64': self.get_user_uidb64,
context={'token': self.change_email_token, 'site_name': settings.SITE_NAME}
'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 reset_password_template(self, country_code): def reset_password_template(self, country_code):
"""Get reset password template""" """Get reset password template"""
context = {'token': self.reset_password_token,
'country_code': country_code}
context.update(self.base_template)
return render_to_string( return render_to_string(
template_name=settings.RESETTING_TOKEN_TEMPLATE, template_name=settings.RESETTING_TOKEN_TEMPLATE,
context={'token': self.key, context=context)
'uidb64': self.user.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI,
'site_name': settings.SITE_NAME,
'country_code': country_code})
@staticmethod def confirm_email_template(self, country_code):
def token_is_valid(user, token): """Get confirm email template"""
"""Check if token is valid""" context = {'token': self.confirm_email_token,
return password_token_generator.check_token(user, 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): def change_email_template(self, country_code):
"""Overdue instance""" """Get change email template"""
self.expiry_datetime = timezone.now() context = {'token': self.change_email_token,
self.save() 'country_code': country_code}
context.update(self.base_template)
return render_to_string(
template_name=settings.CHANGE_EMAIL_TEMPLATE,
context=context)

View File

@ -48,7 +48,7 @@ class UserSerializer(serializers.ModelSerializer):
def validate_email(self, value): def validate_email(self, value):
"""Validate email value""" """Validate email value"""
if value == self.instance.email: if value == self.instance.email:
raise serializers.ValidationError() raise serializers.ValidationError(detail='Equal email address.')
return value return value
def validate_username(self, value): def validate_username(self, value):
@ -58,24 +58,21 @@ class UserSerializer(serializers.ModelSerializer):
raise utils_exceptions.NotValidUsernameError() raise utils_exceptions.NotValidUsernameError()
return value 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): def update(self, instance, validated_data):
""" """Override update method"""
Override update method
"""
if 'email' in validated_data:
validated_data['email_confirmed'] = False
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
# Send verification link on user email for change email address if 'email' in validated_data:
if settings.USE_CELERY: instance.email_confirmed = False
tasks.confirm_new_email_address.delay(instance.id) instance.save()
else: # Send verification link on user email for change email address
tasks.confirm_new_email_address(instance.id) 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 return instance
@ -163,7 +160,7 @@ class ConfirmEmailSerializer(serializers.ModelSerializer):
"""Override validate method""" """Override validate method"""
email_confirmed = self.instance.email_confirmed email_confirmed = self.instance.email_confirmed
if email_confirmed: if email_confirmed:
raise serializers.ValidationError() raise utils_exceptions.EmailConfirmedError()
return attrs return attrs
def update(self, instance, validated_data): def update(self, instance, validated_data):

View File

@ -1,27 +1,18 @@
"""Serializers for account web""" """Serializers for account web"""
from django.conf import settings
from django.contrib.auth import password_validation as password_validators 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 django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from account import models, tasks from account import models
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.methods import username_validator from utils.methods import username_validator
class PasswordResetSerializer(serializers.ModelSerializer): class PasswordResetSerializer(serializers.Serializer):
"""Serializer from model PasswordReset""" """Serializer from model PasswordReset"""
username_or_email = serializers.CharField(required=False, username_or_email = serializers.CharField(required=False,
write_only=True,) write_only=True,)
class Meta:
"""Meta class"""
model = models.ResetPasswordToken
fields = (
'username_or_email',
)
@property @property
def request(self): def request(self):
"""Get request from context""" """Get request from context"""
@ -30,41 +21,29 @@ class PasswordResetSerializer(serializers.ModelSerializer):
def validate(self, attrs): def validate(self, attrs):
"""Override validate method""" """Override validate method"""
user = self.request.user user = self.request.user
username_or_email = attrs.get('username_or_email')
if user.is_anonymous: if not user.is_authenticated:
username_or_email = attrs.get('username_or_email')
if not username_or_email: if not username_or_email:
raise serializers.ValidationError(_('Username or Email not in request body.')) raise serializers.ValidationError(_('username or email not in request body.'))
# Check user in DB
filters = {} filters = {}
if username_validator(username_or_email): if username_validator(username_or_email):
filters.update({'username': username_or_email}) filters.update({'username__icontains': username_or_email})
else: else:
filters.update({'email': username_or_email.lower()}) filters.update({'email__icontains': username_or_email})
user_qs = models.User.objects.filter(**filters)
if user_qs.exists() and filters: if filters:
attrs['user'] = user_qs.first() filters.update({'is_active': True})
else: user_qs = models.User.objects.filter(**filters)
attrs['user'] = user
if not user_qs.exists():
raise utils_exceptions.UserNotFoundError()
user = user_qs.first()
attrs['user'] = user
return attrs 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): class PasswordResetConfirmSerializer(serializers.ModelSerializer):
"""Serializer for model User""" """Serializer for model User"""
@ -73,30 +52,24 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer):
class Meta: class Meta:
"""Meta class""" """Meta class"""
model = models.ResetPasswordToken model = models.User
fields = ('password', ) fields = ('password', )
def validate(self, attrs): def validate_password(self, value):
"""Override validate method""" """Password validation method."""
user = self.instance.user
password = attrs.get('password')
try: try:
# Compare new password with the old ones # 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() raise utils_exceptions.PasswordsAreEqual()
# Validate password # Validate password
password_validators.validate_password(password=password) password_validators.validate_password(password=value)
except serializers.ValidationError as e: except serializers.ValidationError as e:
raise serializers.ValidationError(str(e)) raise serializers.ValidationError(str(e))
else: return value
return attrs
def update(self, instance, validated_data): def update(self, instance, validated_data):
"""Override update method""" """Override update method"""
# Update user password from instance # Update user password from instance
instance.user.set_password(validated_data.get('password')) instance.set_password(validated_data.get('password'))
instance.user.save() instance.save()
# Overdue instance
instance.overdue()
return instance return instance

View File

@ -11,26 +11,37 @@ logger = logging.getLogger(__name__)
@shared_task @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.""" """Send email to user for reset password."""
try: try:
obj = models.ResetPasswordToken.objects.get(id=request_id) user = models.User.objects.get(id=user_id)
user = obj.user
user.send_email(subject=_('Password resetting'), user.send_email(subject=_('Password resetting'),
message=obj.reset_password_template(country_code)) message=user.reset_password_template(country_code))
except: except:
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
f'DETAIL: Exception occurred for ResetPasswordToken instance: ' f'DETAIL: Exception occurred for reset password: '
f'{request_id}') f'{user_id}')
@shared_task @shared_task
def confirm_new_email_address(user_id): def confirm_new_email_address(user_id, country_code):
"""Send email to user new email.""" """Send email to user new email."""
try: try:
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Validate new email address'), user.send_email(subject=_('Validate new email address'),
message=user.change_email_template) message=user.confirm_email_template(country_code))
except: except:
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n' logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}') 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}')

View File

@ -8,7 +8,5 @@ app_name = 'account'
urlpatterns = [ urlpatterns = [
path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'), path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'),
path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), path('change-password/', views.ChangePasswordView.as_view(), name='change-password'),
path('change-email/confirm/<uidb64>/<token>/', views.ChangeEmailConfirmView.as_view(), path('email/confirm/<uidb64>/<token>/', views.ConfirmEmailView.as_view(), name='confirm-email'),
name='change-email-confirm'),
path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'),
] ]

View File

@ -8,10 +8,8 @@ app_name = 'account'
urlpatterns_api = [ urlpatterns_api = [
path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'), path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'),
path('form/reset-password/<uidb64>/<token>/', views.FormPasswordResetConfirmView.as_view(), path('reset-password/confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(),
name='form-password-reset-confirm'), name='password-reset-confirm'),
path('form/reset-password/success/', views.FormPasswordResetSuccessView.as_view(),
name='form-password-reset-success'),
] ]
urlpatterns = urlpatterns_api + \ urlpatterns = urlpatterns_api + \

View File

@ -6,14 +6,12 @@ from rest_framework import generics
from rest_framework import permissions from rest_framework import permissions
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from account import models from account import models
from account.serializers import common as serializers from account.serializers import common as serializers
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator from utils.models import GMTokenGenerator
from utils.views import (JWTUpdateAPIView, from utils.views import JWTGenericViewMixin
JWTGenericViewMixin)
# User views # User views
@ -26,53 +24,27 @@ class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView):
return self.request.user return self.request.user
class ChangePasswordView(JWTUpdateAPIView): class ChangePasswordView(generics.GenericAPIView):
"""Change password view""" """Change password view"""
serializer_class = serializers.ChangePasswordSerializer serializer_class = serializers.ChangePasswordSerializer
queryset = models.User.objects.active() 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): def patch(self, request, *args, **kwargs):
"""Implement PUT method""" """Implement PUT method"""
instance = self.get_object() serializer = self.get_serializer(instance=self.request.user,
serializer = self.get_serializer(instance=instance,
data=request.data) data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
class ConfirmEmailView(JWTGenericViewMixin): class SendConfirmationEmailView(JWTGenericViewMixin):
"""Confirm email view.""" """Confirm email view."""
serializer_class = serializers.ConfirmEmailSerializer serializer_class = serializers.ConfirmEmailSerializer
queryset = models.User.objects.all() queryset = models.User.objects.all()
def patch(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs):
"""Implement POST-method""" """Implement PATCH-method"""
# Get user instance # Get user instance
instance = self.request.user instance = self.request.user
@ -82,7 +54,7 @@ class ConfirmEmailView(JWTGenericViewMixin):
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
class ChangeEmailConfirmView(JWTGenericViewMixin): class ConfirmEmailView(JWTGenericViewMixin):
"""View for confirm changing email""" """View for confirm changing email"""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
@ -95,12 +67,18 @@ class ChangeEmailConfirmView(JWTGenericViewMixin):
user_qs = models.User.objects.filter(pk=uid) user_qs = models.User.objects.filter(pk=uid)
if user_qs.exists(): if user_qs.exists():
user = user_qs.first() user = user_qs.first()
if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( if not GMTokenGenerator(GMTokenGenerator.CONFIRM_EMAIL).check_token(
user, token): user, token):
raise utils_exceptions.NotValidTokenError() raise utils_exceptions.NotValidTokenError()
# Approve email status # Approve email status
user.confirm_email() 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: else:
raise utils_exceptions.UserNotFoundError() raise utils_exceptions.UserNotFoundError()

View File

@ -1,42 +1,35 @@
"""Web account views""" """Web account views"""
from django.conf import settings from django.conf import settings
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator as password_token_generator
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 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.encoding import force_text
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _ from rest_framework import permissions, status, generics
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.response import Response from rest_framework.response import Response
from account import models from account import tasks, models
from account.forms import SetPasswordForm
from account.serializers import web as serializers from account.serializers import web as serializers
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator
from utils.views import JWTGenericViewMixin from utils.views import JWTGenericViewMixin
class PasswordResetView(JWTGenericViewMixin): class PasswordResetView(generics.GenericAPIView):
"""View for resetting user password""" """View for resetting user password"""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )
serializer_class = serializers.PasswordResetSerializer serializer_class = serializers.PasswordResetSerializer
queryset = models.ResetPasswordToken.objects.valid()
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Override create method""" """Override create method"""
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
if serializer.validated_data.get('user'): 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) return Response(status=status.HTTP_200_OK)
@ -44,10 +37,7 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
"""View for confirmation new password""" """View for confirmation new password"""
serializer_class = serializers.PasswordResetConfirmSerializer serializer_class = serializers.PasswordResetConfirmSerializer
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
queryset = models.User.objects.active()
def get_queryset(self):
"""Override get_queryset method"""
return models.ResetPasswordToken.objects.valid()
def get_object(self): def get_object(self):
"""Override get_object method """Override get_object method
@ -58,128 +48,27 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
user_id = force_text(urlsafe_base64_decode(uidb64)) user_id = force_text(urlsafe_base64_decode(uidb64))
token = self.kwargs.get('token') token = self.kwargs.get('token')
filter_kwargs = {'key': token, 'user_id': user_id} obj = get_object_or_404(queryset, id=user_id)
obj = get_object_or_404(queryset, **filter_kwargs)
if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( if not password_token_generator.check_token(user=obj, token=token):
user=obj.user, token=token): raise utils_exceptions.NotValidTokenError()
raise utils_exceptions.NotValidAccessTokenError()
# May raise a permission denied # May raise a permission denied
self.check_object_permissions(self.request, obj) self.check_object_permissions(self.request, obj)
return obj return obj
def put(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs):
"""Implement PUT method""" """Implement PATCH method"""
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance=instance, serializer = self.get_serializer(instance=instance,
data=request.data) data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response(status=status.HTTP_200_OK) # Create tokens
tokens = instance.create_jwt_tokens()
return self._put_cookies_in_response(
# Form view cookies=self._put_data_in_cookies(
class PasswordContextMixin: access_token=tokens.get('access_token'),
extra_context = None refresh_token=tokens.get('refresh_token')),
response=Response(status=status.HTTP_200_OK))
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

View File

@ -29,7 +29,7 @@ urlpatterns_oauth2 = [
urlpatterns_jwt = [ urlpatterns_jwt = [
path('signup/', views.SignUpView.as_view(), name='signup'), path('signup/', views.SignUpView.as_view(), name='signup'),
path('signup/confirm/<uidb64>/<token>/', views.VerifyEmailConfirmView.as_view(), path('signup/confirm/<uidb64>/<token>/', views.ConfirmationEmailView.as_view(),
name='signup-confirm'), name='signup-confirm'),
path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'),
path('logout/', views.LogoutView.as_view(), name="logout"), path('logout/', views.LogoutView.as_view(), name="logout"),

View File

@ -24,13 +24,12 @@ from authorization.serializers import common as serializers
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator from utils.models import GMTokenGenerator
from utils.permissions import IsAuthenticatedAndTokenIsValid from utils.permissions import IsAuthenticatedAndTokenIsValid
from utils.views import (JWTGenericViewMixin, from utils.views import JWTGenericViewMixin
JWTCreateAPIView)
# Mixins # Mixins
# JWTAuthView mixin # JWTAuthView mixin
class JWTAuthViewMixin(JWTCreateAPIView): class JWTAuthViewMixin(JWTGenericViewMixin):
"""Mixin for authentication views""" """Mixin for authentication views"""
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -151,7 +150,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
# JWT # JWT
# Sign in via username and password # Sign in via username and password
class SignUpView(JWTCreateAPIView): class SignUpView(generics.GenericAPIView):
"""View for classic signup""" """View for classic signup"""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )
serializer_class = serializers.SignupSerializer serializer_class = serializers.SignupSerializer
@ -164,7 +163,7 @@ class SignUpView(JWTCreateAPIView):
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)
class VerifyEmailConfirmView(JWTGenericViewMixin): class ConfirmationEmailView(JWTGenericViewMixin):
"""View for confirmation email""" """View for confirmation email"""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )

View File

@ -1,8 +1,9 @@
"""News app common app.""" """News app common app."""
from rest_framework import generics, permissions from rest_framework import generics, permissions
from news import filters, models from news import filters, models
from news.serializers import common as serializers from news.serializers import common as serializers
from utils.views import JWTGenericViewMixin, JWTListAPIView from utils.views import JWTGenericViewMixin
class NewsMixin: class NewsMixin:
@ -18,7 +19,7 @@ class NewsMixin:
.order_by('-is_highlighted', '-created') .order_by('-is_highlighted', '-created')
class NewsListView(NewsMixin, JWTListAPIView): class NewsListView(NewsMixin, generics.ListAPIView):
"""News list view.""" """News list view."""
filter_class = filters.NewsListFilterSet filter_class = filters.NewsListFilterSet

View File

@ -32,15 +32,6 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException):
default_detail = _('User not found') 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): class EmailSendingError(exceptions.APIException):
"""The exception should be thrown when unable to send an email""" """The exception should be thrown when unable to send an email"""
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
@ -142,3 +133,12 @@ class FavoritesError(exceptions.APIException):
""" """
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Item is already in favorites.') 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.')

View File

@ -7,7 +7,7 @@ from rest_framework.response import Response
# JWT # JWT
# Login base view mixin # Login base view mixins
class JWTGenericViewMixin(generics.GenericAPIView): class JWTGenericViewMixin(generics.GenericAPIView):
"""JWT view mixin""" """JWT view mixin"""
@ -95,102 +95,3 @@ class JWTGenericViewMixin(generics.GenericAPIView):
http_only=self.REFRESH_TOKEN_HTTP_ONLY, http_only=self.REFRESH_TOKEN_HTTP_ONLY,
secure=self.REFRESH_TOKEN_SECURE, secure=self.REFRESH_TOKEN_SECURE,
max_age=_cookies.get('max_age'))] 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)

View File

@ -369,7 +369,6 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
# TEMPLATES # TEMPLATES
CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html'
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html' RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html' CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html' CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'

View File

@ -2,9 +2,8 @@
{% blocktrans %}You're receiving this email because you want to change email address at {{ site_name }}.{% endblocktrans %} {% 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:" %} {% 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 %} <a href="https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/</a>
{% endblock %}
{% trans "Thanks for using our site!" %} {% trans "Thanks for using our site!" %}

View File

@ -1,31 +0,0 @@
{% load i18n static %}
{% block content %}
{% if validlink %}
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form method="post">{% csrf_token %}
<fieldset class="module aligned">
<div class="form-row field-password1">
{{ form.new_password1.errors }}
<label for="id_new_password1">{% trans 'New password:' %}</label>
{{ form.new_password1 }}
</div>
<div class="form-row field-password2">
{{ form.new_password2.errors }}
<label for="id_new_password2">{% trans 'Confirm password:' %}</label>
{{ form.new_password2 }}
</div>
<input type="submit" value="{% trans 'Change my password' %}">
</fieldset>
</form>
{% else %}
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %}
{% endblock %}

View File

@ -2,7 +2,9 @@
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} {% 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:" %} {% trans "Please go to the following page and choose a new password:" %}
<a href="https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/</a> <a href="https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/</a>
{% trans "Thanks for using our site!" %} {% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %}