DRF newbie here. I have the following model:
class User(models.Model):
first_name = models.CharField(max_length=30, null=True, blank=True)
last_name = models.CharField(max_length=30, null=True, blank=True)
email = models.EmailField(max_length=254, null=False, blank=False, unique=True)
password = models.CharField(max_length=128, null=False, blank=False)
I've managed to implement POST /users/ successfully. I'm able to validate password and email fields in the request body against pre-defined constraints.(e.g. password cannot be entirely numeric) For this purpose, I override field validators such as validate_password and validate_email.
Now I'm trying to implement an endpoint POST /users/pk/password/ through which users will be able to update their password resource. To achieve this I've used detail_route. Below you can find the corresponding implementation:
# Custom method to update the password resource of a given user.
#detail_route(methods=['post'])
def password(self, request, pk=None):
try:
user = User.objects.get(pk=pk)
except User.DoesNotExist:
# returns error
else:
password = request.data.get('password',None)
new_password = request.data.get('new_password',None)
if password and new_password:
if check_password(password,user.password):
# Use Django built-in to hash password.
password_hash = make_password(new_password)
user.password = password_hash
user.save()
serializer_data = UserSerializer(user).data
return Response(serializer_data)
else:
# return error reponse
else:
# return error response
By using this approach, I'm able to update the password field of a user but validate_password is not effective anymore when POST /users/pk/password/ is called so that users can update their password with an entirely numeric one.
I'm aware of the fact that I can try to implement validate_password inside the detail route implementation, but it does not feel like the best way to go.
My question is what is the best way to do this without duplicating code and without moving validation logic into views.py?
PS: I cannot use nor extend Django User model for various reasons.
Thanks!
This the problem, you are getting the data straight off the raw post, when you are supposed to be using the serializer
password = request.data.get('password',None)
new_password = request.data.get('new_password',None)
Now assuming your serializer with the validation code is called MySerializer, the above lines need to be replaced with something like
serial = MySerializer(data=request.data)
if serial.is_valid():
password = serial.validated_data['password']
new_password = serial.validated_data['new_password']
Now your validation code will be executed.
Related
I have 2 types of users on my site, one is the store owner, I want to log him in with the usual custom user email and password, the other is the buyer, I want to login the buyer using just a pin number only. Is it possible to have both types of login users in the same django app. Thanks in advance.
class Store(models.Model):
store_name = models.CharField(max_length=200)
store_status = models.BooleanField()
store_details = models.CharField(max_length=300, blank = True)
store_balance = models.IntegerField(default=0)
user = models.OneToOneField(User, on_delete=models.CASCADE)
college = models.ForeignKey(Institute, on_delete=models.CASCADE )
def __str__(self):
return str(self.store_name)+" "+ str(self.store_status)
class Customer(models.Model):
name = models.CharField(max_length=200)
branch = models.CharField(max_length=200, choices=BRANCHES)
sem = models.CharField(max_length=200, choices=SEMESTERS)
reg_no = models.IntegerField(default=0)
balance = models.IntegerField(default=0)
pin_no = models.IntegerField()
college = models.ForeignKey(Institute, on_delete=models.CASCADE )
To make a custom authentication you need to add an Authentication Backend. Firstly your customer model is not related to your user model try adding a OnetoOne field in that. After that try adding code like this in one of your apps:-
from django.contrib.auth.backends import BaseBackend
class MyBackend(BaseBackend):
def authenticate(self, request, token=None):
try:
customer = Customer.objects.get(pin_no=token)
user = customer.user
except Customer.DoesNotExist:
return None
return user
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Refer the documentation on Customizing authentication in Django for more information.
Now after making an AuthenticationBackend you need to make it so that Django uses it, you do this by adding it to AUTHENTICATION_BACKENDS in your settings.py, since you want the default username and password to remain something like this would work:
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend', 'path.to.MyBackend']
You have several ways for this. One is making an index place where they choose the option "owner" or "buyer" and after ask for login auth. Other approach is to make url specifying those options already. If you want to do it in the same "form" you could add specification under the hood for manage this input data provided also could be done by several ways, or make a checkbox like so it changes form input. Choose the one that suits you. Does this help you?
I have used django user model and extended it to add a phone field. I was able to use either username or email to login but cannot access phone number field from extended user model.
models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phonenumber = models.CharField(max_length=10)
backends.py
class AuthenticationBackend(backends.ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
usermodel = get_user_model()
print(usermodel)
try:
user = usermodel.objects.get(Q(username__iexact=username) | Q(
email__iexact=username))
if user.check_password(password):
return user
except user.DoesNotExist:
pass
backend.py is what I used to implement login via username or email but I couldn't do it for phonenumber in extended user model.
There are two ways to solve your problem.
Extend the User model by subclassing it, and changing the default User model. I would recommend you go this route, since the Phone number is used for authentication.
Set related_name on Profile, and query the User model from that name.
For the first one, I recommend you check out the Django documentation on creating custom users.
There is also a pretty extensive tutorial on how to do this here.
If you want to go the easy route, you just need to set a reverse accessor on the Profile's phonenumber field.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phonenumber = models.CharField(max_length=10, related_name='profile')
You can then search users by their profile's phonenumbers:
class AuthenticationBackend(backends.ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
usermodel = get_user_model()
print(usermodel)
try:
user = usermodel.objects.get(Q(username__iexact=username) | Q(
email__iexact=username) | Q(profile__phonenumber__iexact=username)
if user.check_password(password):
return user
except user.DoesNotExist:
pass
Although this is not within the scope of your question, I think you should be aware of the following issues with your code:
Phone numbers are often more complex than a 10 character string. +31 6 12345678, is a valid phone number, but it's more than 10 characters. Check out this question for pointers on how to store and validate phone numbers.
If you use a field for authentication, make sure it's unique. The phonenumber field should actually be phonenumber = models.CharField(max_length=10, related_name='profile', unique=True)
Your usermodel.objects.get(...) query will return more than one user if there is a user with phone number '0123456789' and a different user with username '0123456789'. You should constrain usernames to not contain phone numbers, which is only possible by extending the default User class.
You can extend the AbstractUser and add the phonenumber on it.
class User(AbstractUser):
phonenumber = models.CharField(max_length=10)
USERNAME_FIELD = 'phonenumber'
Then you should specify the custom model as the default user model in settings.py
AUTH_USER_MODEL = 'app_name.User'
Customizing authentication in Django
I'm having a really odd issue that I'm having trouble figuring out.
On my site, I have an option for a user to register via either a normal signup page or via Facebook social auth --I'm using the Social Auth App for Python/Django.
A user can successfully register either way.
If a user registers by the normal signup method and enters username and email and password---they are able to successfully trigger a password reset if desired via a password reset page.
BUT, if a user signs up via Facebook AUTH, after their user profile is created, if they go to enter their email for a password reset, no EMAIL is generated.
Here are my settings in settings.py for the auth apps.
AUTH_USER_MODEL = "accounts.User"
SOCIAL_AUTH_USER_MODEL = 'accounts.User'
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'fields': 'id,name,email',
}
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email', # <--- enable this one
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)
As you can see i'm slugifying the username, so all fields are populated.
Here is my User model
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True, error_messages={'unique':"This email has already been registered."})
username = models.CharField(max_length=40, default='')
first_name = models.CharField(max_length=40, default='', blank=True)
last_name = models.CharField(max_length=40, default='', blank=True)
date_joined = models.DateTimeField(default=timezone.now)
favorites = models.ManyToManyField(Deal, related_name='favorited_by', null=True, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
Anyone see anything that I might be missing here--or why these emails might not be triggered?
you need to override get_users method of class auth_views.PasswordResetForm example
class yourOverriding(auth_views.PasswordResetForm):
def get_users(self, email):
"""Given an email, return matching user(s) who should receive a reset.
This allows subclasses to more easily customize the default policies
that prevent inactive users and users with unusable passwords from
resetting their password.
"""
active_users = UserModel._default_manager.filter(**{
'%s__iexact' % UserModel.get_email_field_name(): email,
'is_active': True,
})
d = (u for u in active_users)
return d
By default Django will set an unusable password on the user record, also by default Django won't allow you to reset these users password, for that to happen you need to implement your own version of the reset mechanism (you should be able to extend Django built-in on).
python-social-auth doesn't handle passwords or passwords resets.
I know that some may say that there is no need to provide password in api-response if i have to hide it. but the point is, even if i go to admin section, i can see the password hash value, that's why i'm doing this.
So my question is, how can I hide that password in my API response. For ex. using asterisks.
Note: I have a custom model for my data.
i just need a function and where to put it to work.
models.py
from django.db import models
from django.contrib.auth.hashers import make_password
class MyUser(models.Model):
full_name = models.CharField(max_length=128)
profile_picture = models.ImageField(upload_to="user_data/profile_picture", blank=True)
username = models.CharField(max_length=128, unique=True)
birth_date = models.DateField(blank=True)
gender = models.CharField(max_length=10, blank=True)
password = models.CharField(max_length=255)
contact = models.CharField(max_length=15, blank=True)
email = models.CharField(max_length=100, unique=True)
time_stamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.username
def save(self, *args, **kwargs):
if not self.pk:
self.password = make_password(self.password)
super(MyUser, self).save()
You can provide any number of asterisks you like, or provide the hashed password (user.password). There is no way for you to know what a user's password is, or how many characters are in it, though, so providing somepassword as ************ (same number of characters) is not possible.
If you feel you need to provide something, I recommend just picking an arbitrary number of asterisks.
As an aside, I would strongly suggest you look at the documentation for extending the Django User model, rather than fully rolling your own.
It's better to override the BaseUserManager, AbstractBaseUser from django.contrib.auth.models to work with your custom model. Thus it will act as a default User model(built-in model) and it will do all. Follow this link to the guide(Code) or Follow this link for step by step brief guide(video tutorial)
I want to add a function at the end of the auth pipeline, the function is meant to check if there is a "Profiles" table for that user, if there isn't it will create a table.
The Profiles model is a table where I store some extra information about the user:
class Profiles(models.Model):
user = models.OneToOneField(User, unique=True, null=True)
description = models.CharField(max_length=250, blank=True, null=True)
points = models.SmallIntegerField(default=0)
posts_number = models.SmallIntegerField(default=0)
Each user must have a Profiles table. So, I added a function at the end of the pipeline:
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details',
'app.utils.create_profile' #Custom pipeline
)
#utils.py
def create_profile(strategy, details, response, user, *args, **kwargs):
username = kwargs['details']['username']
user_object = User.objects.get(username=username)
if Profiles.ojects.filter(user=user_object).exists():
pass
else:
new_profile = Profiles(user=user_object)
new_profile.save()
return kwargs
I get the error:
KeyError at /complete/facebook/
'details'
...
utils.py in create_profile
username = kwargs['details']['username']
I'm new to python social auth, and it looks that I'm missing something obvious. Any help will be appreciated.
Ok so I'll answer my own question just in case it is useful for someone in the future. I'm no expert but here it is:
I was following this tutorial, and becouse he does
email = kwargs['details']['email']
I thought I could do
username = kwargs['details']['username']
But it didn't work, it gave my a KeyError.
Then I tried:
username = details['username']
And it worked. But I had a new problem, the username from the details dict was something like u'Firstname Lastname' and when I tried to get the User object
user_object = User.objects.get(username=username)
It was not found, becouse the username in the User model was u'FirstnameLastname' (without the space).
Finally I read the docs again and I found out that I could simply use the user object directly, it gets passed to the function as "user":
def create_profile(strategy, details, response, user, *args, **kwargs):
if Profiles.objects.filter(usuario=user).exists():
pass
else:
new_profile = Profiles(user=user)
new_profile.save()
return kwargs