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')}),
|
||||
(_('Permissions'), {
|
||||
'fields': (
|
||||
'is_active', 'is_staff', 'is_superuser', 'email_confirmed'
|
||||
'is_active', 'is_staff', 'is_superuser', 'email_confirmed',
|
||||
'groups', 'user_permissions'),
|
||||
'classes': ('collapse', )
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
|||
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