added endpoint to change an email

This commit is contained in:
Anatoly 2019-08-30 17:56:35 +03:00
parent f0743fe688
commit f8b87c4788
13 changed files with 197 additions and 34 deletions

View File

@ -27,7 +27,7 @@ class UserAdmin(BaseUserAdmin):
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Permissions'), {
'fields': (
'is_active', 'is_staff', 'is_superuser', 'email_confirmed'
'is_active', 'is_staff', 'is_superuser', 'email_confirmed',
'groups', 'user_permissions'),
'classes': ('collapse', )
}),

View File

@ -15,7 +15,7 @@ from rest_framework.authtoken.models import Token
from authorization.models import Application
from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
from utils.models import gm_token_generator
from utils.models import GMTokenGenerator
class UserManager(BaseUserManager):
@ -126,24 +126,40 @@ class User(ImageMixin, AbstractUser):
@property
def confirm_email_token(self):
"""Make a token for finish signup."""
return gm_token_generator.make_token(self)
return GMTokenGenerator(purpose=GMTokenGenerator.CONFIRM_EMAIL).make_token(self)
@property
def change_email_token(self):
"""Make a token for change email."""
return GMTokenGenerator(purpose=GMTokenGenerator.CHANGE_EMAIL).make_token(self)
@property
def reset_password_token(self):
"""Make a token for finish signup."""
return password_token_generator.make_token(self)
return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self)
@property
def get_user_uidb64(self):
"""Get base64 value for user by primary key identifier"""
return urlsafe_base64_encode(force_bytes(self.pk))
def get_confirm_email_template(self):
@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,
'uid': self.get_user_uidb64,
'uidb64': self.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI,
'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})
@ -221,10 +237,20 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin):
"""Generates a pseudo random code"""
return password_token_generator.make_token(self.user)
@property
def reset_password_template(self):
"""Get reset password template"""
return render_to_string(
template_name=settings.RESETTING_TOKEN_TEMPLATE,
context={'token': self.key,
'uidb64': self.user.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI,
'site_name': settings.SITE_NAME})
@staticmethod
def token_is_valid(user, token):
"""Check if token is valid"""
return gm_token_generator.check_token(user, token)
return password_token_generator.check_token(user, token)
def overdue(self):
"""Overdue instance"""
@ -241,13 +267,3 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin):
if not self.key:
self.key = self.generate_token
return super(ResetPasswordToken, self).save(*args, **kwargs)
def get_reset_password_template(self):
"""Get reset password template"""
return render_to_string(
template_name=settings.RESETTING_TOKEN_TEMPLATE,
context={'token': self.key,
'uidb64': self.user.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI,
'site_name': settings.SITE_NAME})

View File

@ -6,6 +6,7 @@ from account import models
from utils import exceptions as utils_exceptions
from rest_framework_simplejwt import tokens
from django.conf import settings
from account import tasks
# User serializers
@ -98,3 +99,47 @@ class RefreshTokenSerializer(serializers.Serializer):
data['refresh_token'] = str(token)
return data
class ChangeEmailSerializer(serializers.ModelSerializer):
"""Change user email serializer"""
class Meta:
"""Meta class"""
model = models.User
fields = (
'id',
'email',
)
read_only_fields = (
'id',
)
def validate_email(self, value):
"""Validate email value"""
if value == self.instance.email:
# todo: added custom exception
raise serializers.ValidationError()
return value
def validate(self, attrs):
"""Override validate method"""
email_confirmed = self.instance.email_confirmed
if not email_confirmed:
# todo: added custom exception
raise serializers.ValidationError()
return attrs
def update(self, instance, validated_data):
"""
Override update method
"""
instance.email = validated_data.get('email')
instance.email_confirmed = False
instance.save()
# 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)
return instance

View File

@ -17,8 +17,20 @@ def send_reset_password_email(request_id):
obj = models.ResetPasswordToken.objects.get(id=request_id)
user = obj.user
user.send_email(subject=_('Password resetting'),
message=obj.get_reset_password_template())
message=obj.reset_password_template)
except:
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
f'DETAIL: Exception occurred for ResetPasswordToken instance: '
f'{request_id}')
@shared_task
def confirm_new_email_address(user_id):
"""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)
except:
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')

View File

@ -7,5 +7,7 @@ app_name = 'account'
urlpatterns = [
path('user/', views.UserView.as_view(), name='user-get-update'),
path('refresh-token/', views.RefreshTokenView.as_view(), name="refresh-token"),
path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'),
path('change-email/', views.ChangeEmailView.as_view(), name='change-email'),
path('change-email/confirm/<uidb64>/<token>/', views.ChangeEmailConfirmView.as_view(), name='change-email-confirm'),
]

View File

@ -3,6 +3,10 @@ from fcm_django.models import FCMDevice
from rest_framework import generics, status
from rest_framework import permissions
from rest_framework.response import Response
from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_decode
from utils.models import GMTokenGenerator
from utils import exceptions as utils_exceptions
from account import models
from account.serializers import common as serializers
@ -72,3 +76,43 @@ class RefreshTokenView(JWTGenericViewMixin):
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),
response=response)
# Change user email
class ChangeEmailView(JWTGenericViewMixin):
"""Change user email view"""
serializer_class = serializers.ChangeEmailSerializer
queryset = models.User.objects.all()
def patch(self, request, *args, **kwargs):
"""Implement POST-method"""
# Get user instance
instance = self.request.user
serializer = self.get_serializer(data=request.data, instance=instance)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_200_OK)
class ChangeEmailConfirmView(JWTGenericViewMixin):
"""View for confirm changing email"""
permission_classes = (permissions.AllowAny, )
def get(self, request, *args, **kwargs):
"""Implement GET-method"""
uidb64 = kwargs.get('uidb64')
token = kwargs.get('token')
uid = force_text(urlsafe_base64_decode(uidb64))
user_qs = models.User.objects.filter(pk=uid)
if user_qs.exists():
user = user_qs.first()
if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token(
user, token):
raise utils_exceptions.NotValidTokenError()
# Approve email status
user.confirm_email()
return Response(status=status.HTTP_200_OK)
else:
raise utils_exceptions.UserNotFoundError()

View File

@ -21,7 +21,7 @@ from account import models
from account.forms import SetPasswordForm
from account.serializers import web as serializers
from utils import exceptions as utils_exceptions
from utils.models import gm_token_generator
from utils.models import GMTokenGenerator
from utils.views import (JWTCreateAPIView,
JWTGenericViewMixin)
@ -55,7 +55,8 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
filter_kwargs = {'key': token, 'user_id': user_id}
obj = get_object_or_404(queryset, **filter_kwargs)
if not gm_token_generator.check_token(user=obj.user, token=token):
if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token(
user=obj.user, token=token):
raise utils_exceptions.NotValidAccessTokenError()
# May raise a permission denied
@ -89,6 +90,8 @@ class PasswordContextMixin:
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)

View File

@ -123,7 +123,8 @@ class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.Model
username_or_email = attrs.pop('username_or_email')
password = attrs.pop('password')
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
Q(email=username_or_email))
(Q(email=username_or_email) &
Q(email_confirmed=True)))
if not user_qs.exists():
raise utils_exceptions.UserNotFoundError()
else:

View File

@ -15,7 +15,7 @@ def send_confirm_email(user_id):
try:
obj = account_models.User.objects.get(id=user_id)
obj.send_email(subject=_('Email confirmation'),
message=obj.get_confirm_email_template())
message=obj.confirm_email_template)
except:
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')

View File

@ -16,7 +16,7 @@ from rest_framework.response import Response
from rest_framework_simplejwt import tokens as jwt_tokens
from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore
from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer
from utils.models import gm_token_generator
from utils.models import GMTokenGenerator
from account.models import User
from authorization.models import Application
@ -182,7 +182,8 @@ class VerifyEmailConfirmView(JWTGenericViewMixin):
user_qs = User.objects.filter(pk=uid)
if user_qs.exists():
user = user_qs.first()
if not gm_token_generator.check_token(user, token):
if not GMTokenGenerator(GMTokenGenerator.CONFIRM_EMAIL).check_token(
user, token):
raise utils_exceptions.NotValidTokenError()
# Approve email status
user.confirm_email()

View File

@ -189,13 +189,39 @@ class LocaleManagerMixin(models.Manager):
class GMTokenGenerator(PasswordResetTokenGenerator):
CHANGE_EMAIL = 0
RESET_PASSWORD = 1
CHANGE_PASSWORD = 2
CONFIRM_EMAIL = 3
TOKEN_CHOICES = (
CHANGE_EMAIL,
RESET_PASSWORD,
CHANGE_PASSWORD,
CONFIRM_EMAIL
)
def __init__(self, purpose: int):
if purpose in self.TOKEN_CHOICES:
self.purpose = purpose
def get_fields(self, user, timestamp):
"""
Get user fields for hash value.
"""
fields = [str(timestamp), str(user.is_active), str(user.pk)]
if self.purpose == self.CHANGE_EMAIL or \
self.purpose == self.CONFIRM_EMAIL:
fields.extend([str(user.email_confirmed), str(user.email)])
elif self.purpose == self.RESET_PASSWORD or \
self.purpose == self.CHANGE_PASSWORD:
fields.append(str(user.password))
return fields
def _make_hash_value(self, user, timestamp):
return (
str(user.pk) +
str(user.email_confirmed) +
str(timestamp) +
str(user.is_active)
)
gm_token_generator = GMTokenGenerator()
"""
Hash the user's primary key and some user state that's sure to change
after a password reset to produce a token that invalidated when it's
used.
"""
return self.get_fields(user, timestamp)

View File

@ -369,6 +369,7 @@ 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'

View File

@ -0,0 +1,12 @@
{% load i18n %}{% autoescape off %}
{% 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 %}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}