310 lines
9.2 KiB
Markdown
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.
|