HH/docs/USERMANAGER_IMPLEMENTATION.md
2026-01-12 12:19:19 +03:00

9.2 KiB

UserManager Implementation

Overview

This document describes the implementation of a custom UserManager to support email-based authentication in the PX360 application.

Problem Statement

The User model was configured to use email as the USERNAME_FIELD for authentication, but it was still using Django's default UserManager. This caused issues with:

  1. The createsuperuser management command expecting a username instead of email
  2. User creation methods not properly handling email-based authentication
  3. Inconsistent behavior between authentication and user management

Solution

Implemented a custom UserManager that extends Django's BaseUserManager to properly handle email-based authentication.

Changes Made

1. Created UserManager Class

File: apps/accounts/models.py

class UserManager(BaseUserManager):
    """
    Custom user manager for email-based authentication.
    """
    def create_user(self, email, password=None, **extra_fields):
        """
        Create and save a regular user with the given email and password.
        """
        if not email:
            raise ValueError('The Email field must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        """
        Create and save a superuser with the given email and password.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self.create_user(email, password, **extra_fields)

2. Updated User Model

File: apps/accounts/models.py

  • Added UserManager import from django.contrib.auth.models
  • Added optional username field (for backward compatibility)
  • Set objects = UserManager() on the User model
from django.contrib.auth.models import AbstractUser, Group, Permission, BaseUserManager

class User(AbstractUser, TimeStampedModel):
    # ... other fields ...
    
    # Override username to be optional and non-unique (for backward compatibility)
    username = models.CharField(max_length=150, blank=True, null=True, unique=False)
    
    # Use email as username field for authentication
    USERNAME_FIELD = 'email'
    
    # Required fields when creating superuser
    REQUIRED_FIELDS = ['first_name', 'last_name']
    
    # Custom user manager
    objects = UserManager()

3. Updated StaffService

File: apps/organizations/services.py

Modified create_user_for_staff method to work with the new UserManager:

@staticmethod
def create_user_for_staff(staff, role='staff', request=None):
    """
    Create a User account for a Staff member.
    """
    if staff.user:
        raise ValueError("Staff member already has a user account")

    # Generate email (required for authentication)
    if not staff.email:
        raise ValueError("Staff member must have an email address")

    # Generate username (optional, for backward compatibility)
    username = StaffService.generate_username(staff)
    password = StaffService.generate_password()

    # Create user - email is now the username field
    user = User.objects.create_user(
        email=staff.email,  # Email is the first parameter
        password=password,
        first_name=staff.first_name,
        last_name=staff.last_name,
        username=username,  # Optional field
        employee_id=staff.employee_id,
        hospital=staff.hospital,
        department=staff.department,
        is_active=True,
        is_provisional=False
    )
    
    # ... rest of the method ...

Key Changes:

  • email is now the first parameter (required by UserManager)
  • username is passed as an optional field
  • Removed return of password (it's managed separately in the seed command)

4. Updated Seed Staff Command

File: apps/organizations/management/commands/seed_staff.py

Modified create_user_for_staff method:

def create_user_for_staff(self, staff, send_email=False):
    """Create a user account for staff using StaffService"""
    try:
        # Generate password first
        password = StaffService.generate_password()
        
        # Create user account using StaffService
        user = StaffService.create_user_for_staff(staff, role, request)
        
        # Set the generated password (since StaffService doesn't return it anymore)
        user.set_password(password)
        user.save()
        
        # ... rest of the method ...

Key Changes:

  • Generate password before creating user
  • Set password separately after user creation
  • Updated logging to show user.email instead of user.username

Database Migration

Migration File: apps/accounts/migrations/0004_alter_user_managers_and_more.py

This migration:

  • Changes the manager on the User model
  • Alters the acknowledgement_completed_at field
  • Alters the username field to be non-unique and optional

Usage

Creating a Superuser

python manage.py createsuperuser

The command will now prompt for:

  • Email: (required)
  • First name: (required)
  • Last name: (required)
  • Password: (required)

Creating a User Programmatically

from apps.accounts.models import User

# Create a regular user
user = User.objects.create_user(
    email='user@example.com',
    password='securepassword123',
    first_name='John',
    last_name='Doe'
)

# Create a superuser
superuser = User.objects.create_superuser(
    email='admin@example.com',
    password='securepassword123',
    first_name='Admin',
    last_name='User'
)

Creating a User for Staff

from apps.organizations.services import StaffService

# Ensure staff has an email
staff.email = 'staff@example.com'
staff.save()

# Create user account
user = StaffService.create_user_for_staff(
    staff=staff,
    role='staff',
    request=None
)

Testing

Test Superuser Creation

python manage.py createsuperuser

Follow the prompts to create a superuser using email as the identifier.

Test Staff Seeding

# Create staff without users
python manage.py seed_staff --physicians 5 --nurses 5

# Create staff with user accounts
python manage.py seed_staff --physicians 5 --nurses 5 --create-users

# Create staff with user accounts and send emails
python manage.py seed_staff --physicians 5 --nurses 5 --create-users --send-emails

Backward Compatibility

The implementation maintains backward compatibility by:

  1. Keeping the username field (optional, non-unique)
  2. Preserving existing user data
  3. Allowing authentication via email (primary) while maintaining username field for legacy purposes

Authentication Flow

  1. Login: Users authenticate using their email address
  2. Password Reset: Password reset uses email field
  3. User Management: All user operations use email as the primary identifier

Important Notes

  1. Email is Required: All users must have a unique email address
  2. Username is Optional: The username field exists for backward compatibility but is not used for authentication
  3. Password Management: Passwords are still hashed and stored securely using Django's built-in password hashing
  4. Staff Email Requirement: When creating users for staff, the staff member must have an email address

Migration Details

Before Migration

class User(AbstractUser):
    email = models.EmailField(unique=True, db_index=True)
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['first_name', 'last_name']
    objects = BaseUserManager()  # Default Django manager

After Migration

class User(AbstractUser):
    email = models.EmailField(unique=True, db_index=True)
    username = models.CharField(max_length=150, blank=True, null=True, unique=False)
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['first_name', 'last_name']
    objects = UserManager()  # Custom email-based manager

Troubleshooting

Issue: "The Email field must be set"

Solution: Ensure you provide an email address when creating users. The email field is required.

Issue: "Superuser must have is_staff=True"

Solution: This error occurs when using create_superuser without proper parameters. Use User.objects.create_superuser() instead of User.objects.create_user().

Issue: Staff user creation fails

Solution: Ensure the staff member has an email address before calling StaffService.create_user_for_staff().

Summary

The UserManager implementation provides a robust solution for email-based authentication in the PX360 application. It resolves issues with superuser creation, staff user management, and provides a consistent authentication experience across the platform.