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 username and setting USERNAME_FIELD = "mobile" is correct.

  • Using DRF + dj-rest-auth + allauth together 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.

Вернуться на верх