Advice on custom user model and custom auth serializers
I need some advice from the devs out there who are experienced in Django. I am relatively new to this framework and am working on a pharmacy management app with a Django-powered backend. I plan to use mobile number as the way to sign up into the app. Most built-in classes provide email as the default way to register and create new users. I am using DRF for the REST API stuff, dj-rest-auth and django-allauth for handling authentication. Right now I am working only on user accounts within the app. Could you please give a check and point out the mistakes. Any advice on the best practices would be appreciated.
Here's the custom user model
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .managers import CustomUserManager
from django.core.validators import RegexValidator
phone_validator = RegexValidator(regex=r"^[1-9]\d{9}$", message="Invalid Number")
class UserRole(models.TextChoices):
RETAILER = "RETAILER", "retailer"
STAFF = "STAFF", "staff"
class CustomUser(AbstractUser):
username = None
mobile = models.CharField(max_length=10, validators=[phone_validator], unique=True)
role = models.CharField(
max_length=20, choices=UserRole.choices, default=UserRole.STAFF
)
tenant = models.ForeignKey(
"tenants.Tenant",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="users",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = CustomUserManager()
USERNAME_FIELD = "mobile"
REQUIRED_FIELDS = []
def __str__(self):
return f"{self.mobile} ({self.get_full_name()})"
Here are the custom serializers.
from dj_rest_auth.serializers import LoginSerializer
from dj_rest_auth.registration.serializers import RegisterSerializer
from rest_framework import serializers
from allauth.account.adapter import get_adapter
class MobileLoginSerializer(LoginSerializer):
username = None
mobile = serializers.CharField(required=True)
def validate(self, attrs):
attrs["username"] = attrs.get("mobile")
return super().validate(attrs)
class MobileRegisterSerializer(RegisterSerializer):
username = None
first_name = serializers.CharField()
last_name = serializers.CharField()
mobile = serializers.CharField()
pharmacy_name = serializers.CharField(write_only=True)
role = serializers.ChoiceField(choices=["RETAILER", "STAFF"])
def get_cleaned_data(self):
data = super().get_cleaned_data()
data.update(
{
"first_name": self.validated_data.get("first_name", ""),
"last_name": self.validated_data.get("last_name", ""),
"mobile": self.validated_data.get("mobile", ""),
"pharmacy_name": self.validated_data.get("pharmacy_name", ""),
"role": self.validated_data.get("role", ""),
}
)
return data
def save(self, request):
adapter = get_adapter()
user = adapter.new_user(request)
self.cleaned_data = self.get_cleaned_data()
user = adapter.save_user(request, user, self, commit=False)
user.first_name = self.cleaned_data.get("first_name")
user.last_name = self.cleaned_data.get("last_name")
user.mobile = self.cleaned_data.get("mobile")
# user.pharmacy_name = self.cleaned_data.get("pharmacy_name")
user.role = self.cleaned_data.get("role")
user.save()
self.custom_signup(request, user)
return user
Here are the relevant code snippets from settings.py
...
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ["dj_rest_auth.jwt_auth.JWTCookieAuthentication"]
}
REST_AUTH = {
"USE_JWT": True,
"JWT_AUTH_COOKIE": "access",
"JWT_AUTH_REFRESH_COOKIE": "refresh",
"JWT_AUTH_HTTPONLY": True,
"LOGIN_SERIALIZER": "accounts.serializers.MobileLoginSerializer",
"REGISTER_SERIALIZER": "accounts.serializers.MobileRegisterSerializer",
}
AUTH_USER_MODEL = "accounts.CustomUser"
ACCOUNT_USER_MODEL_USERNAME_FIELD = "mobile"
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = "username"
...
SITE_ID = 1
P.S.: I do not trust the AI responses. Different agents are suggesting different approaches and now I am confused.
by the way: maybe use documentation or Google Search instead of AI. python - What's the best way to store a phone number in Django models? - Stack Overflow
Your overall approach is good and you're already following the correct direction for a mobile-based authentication system in Django.
A few observations and recommendations:
Good Things in Your Setup
Using a custom user model from the beginning is the right choice.
Removing
usernameand settingUSERNAME_FIELD = "mobile"is correct.Using DRF +
dj-rest-auth+allauthtogether is a common production setup.Your custom login serializer approach is valid.
Important Improvements
1. Make sure your CustomUserManager properly supports mobile login
This is one of the most important parts when removing username.
Example:
from django.contrib.auth.base_user import BaseUserManager
class CustomUserManager(BaseUserManager):
def create_user(self, mobile, password=None, **extra_fields):
if not mobile:
raise ValueError("Mobile number is required")
user = self.model(mobile=mobile, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, mobile, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
return self.create_user(mobile, password, **extra_fields)
Without a proper manager, createsuperuser and authentication can behave unexpectedly.
2. Your current phone validator is very restrictive
regex=r"^[1-9]\d{9}$"
This only supports Indian 10-digit numbers.
If the app may scale later, consider using django-phonenumber-field instead of manual regex validation.
3. ACCOUNT_AUTHENTICATION_METHOD = "username" looks confusing but is acceptable here
Since you're internally mapping:
attrs["username"] = attrs.get("mobile")
your setup works correctly with allauth.
Still, you should also add:
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
because the model no longer contains a username field.
4. Registration serializer can be simplified
Instead of manually calling adapter.new_user() and assigning every field manually, you can usually extend super().save(request) and update fields afterward.
Your current implementation works, but it's more verbose than necessary.
5. Be careful with role assignment
Right now users can register themselves as:
"RETAILER"
or
"STAFF"
directly from the API.
Usually roles like STAFF should be controlled by the backend/admin only.
Overall, your architecture is correct and production-friendly. The main thing I would strongly recommend is ensuring the custom user manager is implemented properly and tightening role/security validation.
That really helped a lot! I will keep the things that you mentioned in mind while creating the remaining data models for the app.
As far as the role assignment goes, I want the user to choose whether they are a retailer or a staff during sign-up. The staff, once registers, will have to get approval from the concerned retailer to be able to use the app. Since, I wanna build a multi-tenant system, that's the plan.