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

310 lines
9.2 KiB
Markdown

# 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`
```python
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
```python
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:
```python
@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:
```python
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
```bash
python manage.py createsuperuser
```
The command will now prompt for:
- Email: (required)
- First name: (required)
- Last name: (required)
- Password: (required)
### Creating a User Programmatically
```python
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
```python
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
```bash
python manage.py createsuperuser
```
Follow the prompts to create a superuser using email as the identifier.
### Test Staff Seeding
```bash
# 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
```python
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
```python
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()`.
## Related Documentation
- [Staff User Account Implementation](./STAFF_USER_ACCOUNT_IMPLEMENTATION.md)
- [Staff Seed Command Update](./STAFF_SEED_COMMAND_UPDATE.md)
- [Login/Logout Functionality Check](../LOGIN_LOGOUT_FUNCTIONALITY_CHECK.md)
## 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.