# -*- coding: utf-8 -*-
"""Serializer implementations for authentication.
"""
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import get_user_model, login, password_validation
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMultiAlternatives
from django.template import loader
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
UserModel = get_user_model()
[docs]class LoginSerializer(serializers.Serializer):
"""Serializer for loggin in.
It checks username and password are correct for settings.AUTH_USER_MODEL.
After validating it, ``user`` instance created for authenticated user.
View methods should persist this user.
(through ``django.contrib.auth.login``)
:param username: ``USERNAME_FIELD`` for AUTH_USER_MODEL
:param password: user's password
"""
username = serializers.CharField(
label=_('Username'), max_length=254,
)
password = serializers.CharField(
label=_('Password'), write_only=True,
style={'input_type': 'password'},
)
default_error_messages = {
'invalid_login': _(
'Please enter a correct username and password. '
'Note that both fields may be case-sensitive.'
),
'inactive': _('This account is inactive.'),
}
[docs] def validate(self, data):
"""Checks username & password.
uses ``django.contrib.auth.authenticate``
:param data: validated data from ``Serializer.validate``
:return: validated_data
:exception VaildationError: if username or password are incorrect
"""
username = data['username']
password = data['password']
self.user = auth.authenticate(username=username, password=password)
if self.user is None:
raise serializers.ValidationError(
self.error_messages['invalid_login'], code='invalid_login',
)
self.confirm_login_allowed(self.user)
return data
[docs] def confirm_login_allowed(self, user):
"""Checks if validated user is allowed for website.
Override this method if you use custom authentication method
and have additional methods for allowing login.
:exception VaildationError: if user are not allowed
"""
[docs] def create(self, validated_data):
"""persist a authenticated user in this step.
:param validated_data: validated_data should contains ``request``.\
You should pass request to serialzer.save.
"""
user = self.get_user()
request = validated_data.get('request')
self.perform_login(request, user)
return user
[docs] def get_user(self):
"""
:return: ``user`` instance created after ``self.validate``
"""
return self.user
[docs]class PasswordResetSerializer(serializers.Serializer):
"""Sends a website link for resetting password.
It uses django's ``PasswordResetForm`` directly because
there is just one required field, `email`, and form implemented
its business logic nicely.
:param email: email address to receive password-reset-link.
"""
email = serializers.EmailField(
label=_('Email'), max_length=254,
)
password_reset_form_class = PasswordResetForm
[docs] def validate_email(self, value):
"""
:exception VaildationError: ``rest_framework``'s field validation
:exception VaildationError: ``django``'s field vaildation
"""
self.form = self.password_reset_form_class(data=self.initial_data)
if not self.form.is_valid():
if 'email' in self.form.errors:
messages = self.form.errors['email']
else:
# XXX non email errors should be catched & re-raised
# (if django's PasswordResetForm add new fields)
messages = self.form.errors
raise serializers.ValidationError(messages)
return value
[docs] def save(self, domain_override=None,
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
use_https=True, token_generator=default_token_generator,
from_email=None, request=None, html_email_template_name=None,
extra_email_context=None):
"""sends a email, which contains link for resetting password
"""
return self.form.save(
domain_override=domain_override,
subject_template_name=subject_template_name,
email_template_name=email_template_name, use_https=use_https,
token_generator=token_generator, from_email=from_email,
request=request, html_email_template_name=html_email_template_name,
extra_email_context=extra_email_context,
)
[docs]class SetPasswordSerializer(serializers.Serializer):
"""This serializer resets password of a given user.
Please be VERY CAREFUL for using this any given user's password
can be changed.
Setting permission IsAdminUser is recommended.
:param new_password1: new password
:param new_password2: new password confirmation.
"""
new_password1 = serializers.CharField(
label=_('New password'),
validators=[password_validation.validate_password],
help_text=password_validation.password_validators_help_text_html(),
write_only=True,
style={'input_type': 'password'},
)
new_password2 = serializers.CharField(
label=_('New password confirmation'),
help_text=_('Enter the same password as before, for verification.'),
write_only=True,
style={'input_type': 'password'},
)
default_error_messages = {
'password_mismatch': _('2 passwords should be equal'),
}
def __init__(self, user, *args, **kwargs):
self.user = user
super(SetPasswordSerializer, self).__init__(*args, **kwargs)
[docs] def validate(self, data):
"""
:exception VaildationError: if two given passwords are different.
"""
password1 = data.get('new_password1')
password2 = data.get('new_password2')
data['new_password2'] =\
self._validate_new_password2(password1, password2)
return data
def _validate_new_password2(self, password1, password2):
if password1 != password2:
raise serializers.ValidationError(
self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2
[docs] def create(self, validated_data):
"""resets password
"""
password = validated_data['new_password1']
self.user.set_password(password)
self.user.save()
return self.user
[docs]class PasswordChangeSerializer(SetPasswordSerializer):
"""resets password of user.
Resetting password is done if old_password is correct and
two new passwords are equals.
:param old_password: old_password
:param new_password1: new password
:param new_password2: new password confirmation.
"""
old_password = serializers.CharField(
label=_('Old password'),
write_only=True,
style={'input_type': 'password'},
)
default_error_messages = {
'password_incorrect': _('Your old password was entered incorrectly. '
'Please enter it again.'),
}
[docs] def validate_old_password(self, old_password):
"""
:exception ValidationError: if old_password is not correct
"""
if not self.user.check_password(old_password):
raise serializers.ValidationError(
self.error_messages['password_incorrect'],
code='password_incorrect'
)
return old_password
[docs]class SignupSerializer(serializers.ModelSerializer):
"""Signup serializer for rest_framework & AUTH_USER_MODEL.
Fields & methods are built on a django's defualt ``User`` model.
Extend this serializer if you need your custom user model.
(Even if ``AUTH_USER_MODEL`` is can be customized, this is recommended
that You don't change & use customized user model.
using custom user model is very complex.)
:param username: ``USERNAME_FIELD`` of ``AUTH_USER_MODEL``
:param email: ``User.get_email_field_name()``
:param password1: password of a user (write_only, used only when created)
:param password2: password confirmation (write_only)
:TODO: Serializer Only implements creating. list/get are need to be implmtd
"""
password1 = serializers.CharField(
label=_('Password'),
validators=[password_validation.validate_password],
help_text=password_validation.password_validators_help_text_html(),
write_only=True,
style={'input_type': 'password'},
)
password2 = serializers.CharField(
label=_('Password Confirmation'),
help_text=_('Enter the same password as before, for verification.'),
write_only=True,
style={'input_type': 'password'},
)
default_error_messages = {
'password_mismatch': _('2 passwords should be equal'),
}
EMAIL_FIELD_NAME = UserModel.get_email_field_name()
class Meta:
model = UserModel
fields = (
UserModel.USERNAME_FIELD, UserModel.get_email_field_name(),
'password1', 'password2',
)
extra_kwargs = {
UserModel.get_email_field_name(): {
'required': True,
'allow_blank': False,
},
}
[docs] def validate(self, data):
"""Vaildates if two passwords are equal.
:exception ValidationError: when 2 passwds are different
"""
password1 = data.get('password1')
password2 = data.get('password2')
data['password2'] = self._validate_password2(password1, password2)
return data
def _validate_password2(self, password1, password2):
if password1 != password2:
raise serializers.ValidationError(
self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2
[docs] def create(self, validated_data):
"""Creates user instance
CAVEAT:
A clear difference between django's ``ModelForm`` and rest_framework's
``ModelSerializer`` is that, model serializer's ``save`` method doesn't
respect form's ``commit=True``.
Inside ``super().create``, a query is fired to create user,
and inside this, additional query is fired to save hashed password.
It's because ``ModelSerializer``'s ``create`` method uses
default manager's create function, ``Model._default_manager.create()``
(User model creation is recommended by calling ``UserManager``'s
``create_user`` method)
:param validated_data: validated data created after ``self.vaildate``
"""
password = validated_data.pop('password1')
email_opts = validated_data.pop('email_opts', {})
validated_data.pop('password2')
# NOTE We should set user's password manually because
# ModelSerializer.create calls model._default_manager.save().
user = super(SignupSerializer, self).create(validated_data)
user.set_password(password)
# user activation through email confirmation.
require_email_confirmation =\
settings.REST_AUTH_SIGNUP_REQUIRE_EMAIL_CONFIRMATION
update_fields = ['password']
if require_email_confirmation:
user, new_update_fields = self.set_user_as_unverified(user)
update_fields.extend(new_update_fields)
self.send_mail(user, **email_opts)
user.save(update_fields=update_fields)
return user
def set_user_as_unverified(self, user):
user.is_active = False
return user, ['is_active']
[docs] def send_mail(self, user, domain_override=None,
subject_template_name='registration/verify_email.txt',
email_template_name='registration/verify_email.html',
use_https=False, token_generator=default_token_generator,
from_email=None, request=None, html_email_template_name=None,
extra_email_context=None):
"""Send verification mail to newbie.
"""
email = self.validated_data[self.EMAIL_FIELD_NAME]
if domain_override:
site_name = domain = domain_override
else:
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
protocol = 'https' if use_https else 'http'
context = {
'email': email, 'domain': domain, 'site_name': site_name,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'user': user,
'token': token_generator.make_token(user), 'protocol': protocol,
}
if extra_email_context is not None:
context.update(extra_email_context)
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
email_message = EmailMultiAlternatives(
subject, body, from_email, [email]
)
if html_email_template_name is not None:
html_email = loader.render_to_string(
html_email_template_name, context
)
email_message.attach_alternative(html_email, 'text/html')
email_message.send()