added endpoint to change an email
This commit is contained in:
parent
f0743fe688
commit
f8b87c4788
|
|
@ -27,7 +27,7 @@ class UserAdmin(BaseUserAdmin):
|
||||||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||||
(_('Permissions'), {
|
(_('Permissions'), {
|
||||||
'fields': (
|
'fields': (
|
||||||
'is_active', 'is_staff', 'is_superuser', 'email_confirmed'
|
'is_active', 'is_staff', 'is_superuser', 'email_confirmed',
|
||||||
'groups', 'user_permissions'),
|
'groups', 'user_permissions'),
|
||||||
'classes': ('collapse', )
|
'classes': ('collapse', )
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from authorization.models import Application
|
from authorization.models import Application
|
||||||
from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
|
from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
|
||||||
from utils.models import gm_token_generator
|
from utils.models import GMTokenGenerator
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
|
|
@ -126,24 +126,40 @@ class User(ImageMixin, AbstractUser):
|
||||||
@property
|
@property
|
||||||
def confirm_email_token(self):
|
def confirm_email_token(self):
|
||||||
"""Make a token for finish signup."""
|
"""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
|
@property
|
||||||
def reset_password_token(self):
|
def reset_password_token(self):
|
||||||
"""Make a token for finish signup."""
|
"""Make a token for finish signup."""
|
||||||
return password_token_generator.make_token(self)
|
return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).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 get_confirm_email_template(self):
|
@property
|
||||||
|
def confirm_email_template(self):
|
||||||
"""Get confirm email template"""
|
"""Get confirm email template"""
|
||||||
return render_to_string(
|
return render_to_string(
|
||||||
template_name=settings.CONFIRM_EMAIL_TEMPLATE,
|
template_name=settings.CONFIRM_EMAIL_TEMPLATE,
|
||||||
context={'token': self.confirm_email_token,
|
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,
|
'domain_uri': settings.DOMAIN_URI,
|
||||||
'site_name': settings.SITE_NAME})
|
'site_name': settings.SITE_NAME})
|
||||||
|
|
||||||
|
|
@ -221,10 +237,20 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin):
|
||||||
"""Generates a pseudo random code"""
|
"""Generates a pseudo random code"""
|
||||||
return password_token_generator.make_token(self.user)
|
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
|
@staticmethod
|
||||||
def token_is_valid(user, token):
|
def token_is_valid(user, token):
|
||||||
"""Check if token is valid"""
|
"""Check if token is valid"""
|
||||||
return gm_token_generator.check_token(user, token)
|
return password_token_generator.check_token(user, token)
|
||||||
|
|
||||||
def overdue(self):
|
def overdue(self):
|
||||||
"""Overdue instance"""
|
"""Overdue instance"""
|
||||||
|
|
@ -241,13 +267,3 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin):
|
||||||
if not self.key:
|
if not self.key:
|
||||||
self.key = self.generate_token
|
self.key = self.generate_token
|
||||||
return super(ResetPasswordToken, self).save(*args, **kwargs)
|
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})
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from account import models
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from rest_framework_simplejwt import tokens
|
from rest_framework_simplejwt import tokens
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from account import tasks
|
||||||
|
|
||||||
|
|
||||||
# User serializers
|
# User serializers
|
||||||
|
|
@ -98,3 +99,47 @@ class RefreshTokenSerializer(serializers.Serializer):
|
||||||
data['refresh_token'] = str(token)
|
data['refresh_token'] = str(token)
|
||||||
|
|
||||||
return data
|
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
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,20 @@ def send_reset_password_email(request_id):
|
||||||
obj = models.ResetPasswordToken.objects.get(id=request_id)
|
obj = models.ResetPasswordToken.objects.get(id=request_id)
|
||||||
user = obj.user
|
user = obj.user
|
||||||
user.send_email(subject=_('Password resetting'),
|
user.send_email(subject=_('Password resetting'),
|
||||||
message=obj.get_reset_password_template())
|
message=obj.reset_password_template)
|
||||||
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 ResetPasswordToken instance: '
|
||||||
f'{request_id}')
|
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}')
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,7 @@ app_name = 'account'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('user/', views.UserView.as_view(), name='user-get-update'),
|
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'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ from fcm_django.models import FCMDevice
|
||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.response import Response
|
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 import models
|
||||||
from account.serializers import common as serializers
|
from account.serializers import common as serializers
|
||||||
|
|
@ -72,3 +76,43 @@ class RefreshTokenView(JWTGenericViewMixin):
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token,
|
cookies=self._put_data_in_cookies(access_token=access_token,
|
||||||
refresh_token=refresh_token),
|
refresh_token=refresh_token),
|
||||||
response=response)
|
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()
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from account import models
|
||||||
from account.forms import SetPasswordForm
|
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 gm_token_generator
|
from utils.models import GMTokenGenerator
|
||||||
from utils.views import (JWTCreateAPIView,
|
from utils.views import (JWTCreateAPIView,
|
||||||
JWTGenericViewMixin)
|
JWTGenericViewMixin)
|
||||||
|
|
||||||
|
|
@ -55,7 +55,8 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
|
||||||
filter_kwargs = {'key': token, 'user_id': user_id}
|
filter_kwargs = {'key': token, 'user_id': user_id}
|
||||||
obj = get_object_or_404(queryset, **filter_kwargs)
|
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()
|
raise utils_exceptions.NotValidAccessTokenError()
|
||||||
|
|
||||||
# May raise a permission denied
|
# May raise a permission denied
|
||||||
|
|
@ -89,6 +90,8 @@ class PasswordContextMixin:
|
||||||
class FormPasswordResetSuccessView(views.APIView):
|
class FormPasswordResetSuccessView(views.APIView):
|
||||||
"""View for successful reset password"""
|
"""View for successful reset password"""
|
||||||
|
|
||||||
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Implement GET-method"""
|
"""Implement GET-method"""
|
||||||
return Response(status=status.HTTP_200_OK)
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,8 @@ class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.Model
|
||||||
username_or_email = attrs.pop('username_or_email')
|
username_or_email = attrs.pop('username_or_email')
|
||||||
password = attrs.pop('password')
|
password = attrs.pop('password')
|
||||||
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
|
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():
|
if not user_qs.exists():
|
||||||
raise utils_exceptions.UserNotFoundError()
|
raise utils_exceptions.UserNotFoundError()
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ def send_confirm_email(user_id):
|
||||||
try:
|
try:
|
||||||
obj = account_models.User.objects.get(id=user_id)
|
obj = account_models.User.objects.get(id=user_id)
|
||||||
obj.send_email(subject=_('Email confirmation'),
|
obj.send_email(subject=_('Email confirmation'),
|
||||||
message=obj.get_confirm_email_template())
|
message=obj.confirm_email_template)
|
||||||
except:
|
except:
|
||||||
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
|
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
|
||||||
f'DETAIL: Exception occurred for user: {user_id}')
|
f'DETAIL: Exception occurred for user: {user_id}')
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from rest_framework.response import Response
|
||||||
from rest_framework_simplejwt import tokens as jwt_tokens
|
from rest_framework_simplejwt import tokens as jwt_tokens
|
||||||
from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore
|
from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore
|
||||||
from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer
|
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 account.models import User
|
||||||
from authorization.models import Application
|
from authorization.models import Application
|
||||||
|
|
@ -182,7 +182,8 @@ class VerifyEmailConfirmView(JWTGenericViewMixin):
|
||||||
user_qs = User.objects.filter(pk=uid)
|
user_qs = User.objects.filter(pk=uid)
|
||||||
if user_qs.exists():
|
if user_qs.exists():
|
||||||
user = user_qs.first()
|
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()
|
raise utils_exceptions.NotValidTokenError()
|
||||||
# Approve email status
|
# Approve email status
|
||||||
user.confirm_email()
|
user.confirm_email()
|
||||||
|
|
|
||||||
|
|
@ -189,13 +189,39 @@ class LocaleManagerMixin(models.Manager):
|
||||||
|
|
||||||
class GMTokenGenerator(PasswordResetTokenGenerator):
|
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):
|
def _make_hash_value(self, user, timestamp):
|
||||||
return (
|
"""
|
||||||
str(user.pk) +
|
Hash the user's primary key and some user state that's sure to change
|
||||||
str(user.email_confirmed) +
|
after a password reset to produce a token that invalidated when it's
|
||||||
str(timestamp) +
|
used.
|
||||||
str(user.is_active)
|
"""
|
||||||
)
|
return self.get_fields(user, timestamp)
|
||||||
|
|
||||||
|
|
||||||
gm_token_generator = GMTokenGenerator()
|
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
|
||||||
# TEMPLATES
|
# TEMPLATES
|
||||||
CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html'
|
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'
|
||||||
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
12
project/templates/account/change_email.html
Normal file
12
project/templates/account/change_email.html
Normal 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 %}
|
||||||
Loading…
Reference in New Issue
Block a user