710 lines
19 KiB
Python
710 lines
19 KiB
Python
"""
|
|
Accounts app models for hospital management system.
|
|
Provides user management, authentication, and authorization functionality.
|
|
"""
|
|
|
|
import uuid
|
|
from django.contrib.auth.models import AbstractUser
|
|
from django.db import models
|
|
from django.core.validators import RegexValidator
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
|
|
|
|
class User(AbstractUser):
|
|
"""
|
|
Extended user model for hospital management system.
|
|
"""
|
|
|
|
# Basic Information
|
|
user_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique user identifier'
|
|
)
|
|
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='users',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# Personal Information
|
|
middle_name = models.CharField(
|
|
max_length=150,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Middle name'
|
|
)
|
|
preferred_name = models.CharField(
|
|
max_length=150,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Preferred name'
|
|
)
|
|
|
|
# Contact Information
|
|
phone_number = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
validators=[RegexValidator(
|
|
regex=r'^\+?1?\d{9,15}$',
|
|
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
|
|
)],
|
|
help_text='Primary phone number'
|
|
)
|
|
mobile_number = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
validators=[RegexValidator(
|
|
regex=r'^\+?1?\d{9,15}$',
|
|
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
|
|
)],
|
|
help_text='Mobile phone number'
|
|
)
|
|
|
|
# Professional Information
|
|
employee_id = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Employee ID'
|
|
)
|
|
department = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Department'
|
|
)
|
|
job_title = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Job title'
|
|
)
|
|
|
|
# Role and Permissions
|
|
role = models.CharField(
|
|
max_length=50,
|
|
choices=[
|
|
('SUPER_ADMIN', 'Super Administrator'),
|
|
('ADMIN', 'Administrator'),
|
|
('PHYSICIAN', 'Physician'),
|
|
('NURSE', 'Nurse'),
|
|
('NURSE_PRACTITIONER', 'Nurse Practitioner'),
|
|
('PHYSICIAN_ASSISTANT', 'Physician Assistant'),
|
|
('PHARMACIST', 'Pharmacist'),
|
|
('PHARMACY_TECH', 'Pharmacy Technician'),
|
|
('LAB_TECH', 'Laboratory Technician'),
|
|
('RADIOLOGIST', 'Radiologist'),
|
|
('RAD_TECH', 'Radiology Technician'),
|
|
('THERAPIST', 'Therapist'),
|
|
('SOCIAL_WORKER', 'Social Worker'),
|
|
('CASE_MANAGER', 'Case Manager'),
|
|
('BILLING_SPECIALIST', 'Billing Specialist'),
|
|
('REGISTRATION', 'Registration Staff'),
|
|
('SCHEDULER', 'Scheduler'),
|
|
('MEDICAL_ASSISTANT', 'Medical Assistant'),
|
|
('CLERICAL', 'Clerical Staff'),
|
|
('IT_SUPPORT', 'IT Support'),
|
|
('QUALITY_ASSURANCE', 'Quality Assurance'),
|
|
('COMPLIANCE', 'Compliance Officer'),
|
|
('SECURITY', 'Security'),
|
|
('MAINTENANCE', 'Maintenance'),
|
|
('VOLUNTEER', 'Volunteer'),
|
|
('STUDENT', 'Student'),
|
|
('RESEARCHER', 'Researcher'),
|
|
('CONSULTANT', 'Consultant'),
|
|
('VENDOR', 'Vendor'),
|
|
('GUEST', 'Guest'),
|
|
],
|
|
default='CLERICAL'
|
|
)
|
|
|
|
# License and Certification
|
|
license_number = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Professional license number'
|
|
)
|
|
license_state = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='License issuing state'
|
|
)
|
|
license_expiry = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='License expiry date'
|
|
)
|
|
dea_number = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='DEA number for prescribing'
|
|
)
|
|
npi_number = models.CharField(
|
|
max_length=10,
|
|
blank=True,
|
|
null=True,
|
|
help_text='National Provider Identifier'
|
|
)
|
|
|
|
# Security Settings
|
|
force_password_change = models.BooleanField(
|
|
default=False,
|
|
help_text='User must change password on next login'
|
|
)
|
|
password_expires_at = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Password expiration date'
|
|
)
|
|
failed_login_attempts = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Number of failed login attempts'
|
|
)
|
|
locked_until = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Account locked until this time'
|
|
)
|
|
two_factor_enabled = models.BooleanField(
|
|
default=False,
|
|
help_text='Two-factor authentication enabled'
|
|
)
|
|
|
|
# Session Management
|
|
max_concurrent_sessions = models.PositiveIntegerField(
|
|
default=3,
|
|
help_text='Maximum concurrent sessions allowed'
|
|
)
|
|
session_timeout_minutes = models.PositiveIntegerField(
|
|
default=30,
|
|
help_text='Session timeout in minutes'
|
|
)
|
|
|
|
# Preferences
|
|
user_timezone = models.CharField(
|
|
max_length=50,
|
|
default='UTC',
|
|
help_text='User timezone'
|
|
)
|
|
language = models.CharField(
|
|
max_length=10,
|
|
default='en',
|
|
help_text='Preferred language'
|
|
)
|
|
theme = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('LIGHT', 'Light'),
|
|
('DARK', 'Dark'),
|
|
('AUTO', 'Auto'),
|
|
],
|
|
default='LIGHT'
|
|
)
|
|
|
|
# Profile Information
|
|
profile_picture = models.ImageField(
|
|
upload_to='profile_pictures/',
|
|
blank=True,
|
|
null=True,
|
|
help_text='Profile picture'
|
|
)
|
|
bio = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Professional bio'
|
|
)
|
|
|
|
# Status
|
|
is_verified = models.BooleanField(
|
|
default=False,
|
|
help_text='User account is verified'
|
|
)
|
|
is_approved = models.BooleanField(
|
|
default=False,
|
|
help_text='User account is approved'
|
|
)
|
|
approval_date = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Account approval date'
|
|
)
|
|
approved_by = models.ForeignKey(
|
|
'self',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='approved_users',
|
|
help_text='User who approved this account'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
last_password_change = models.DateTimeField(
|
|
default=timezone.now,
|
|
help_text='Last password change date'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'accounts_user'
|
|
verbose_name = 'User'
|
|
verbose_name_plural = 'Users'
|
|
ordering = ['last_name', 'first_name']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'role']),
|
|
models.Index(fields=['employee_id']),
|
|
models.Index(fields=['license_number']),
|
|
models.Index(fields=['npi_number']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.get_full_name()} ({self.username})"
|
|
|
|
def get_full_name(self):
|
|
"""
|
|
Return the full name for the user.
|
|
"""
|
|
if self.preferred_name:
|
|
return f"{self.preferred_name} {self.last_name}"
|
|
return super().get_full_name()
|
|
|
|
def get_display_name(self):
|
|
"""
|
|
Return the display name for the user.
|
|
"""
|
|
full_name = self.get_full_name()
|
|
if full_name.strip():
|
|
return full_name
|
|
return self.username
|
|
|
|
@property
|
|
def is_account_locked(self):
|
|
"""
|
|
Check if account is currently locked.
|
|
"""
|
|
if self.locked_until:
|
|
return timezone.now() < self.locked_until
|
|
return False
|
|
|
|
@property
|
|
def is_password_expired(self):
|
|
"""
|
|
Check if password has expired.
|
|
"""
|
|
if self.password_expires_at:
|
|
return timezone.now() > self.password_expires_at
|
|
return False
|
|
|
|
@property
|
|
def is_license_expired(self):
|
|
"""
|
|
Check if professional license has expired.
|
|
"""
|
|
if self.license_expiry:
|
|
return timezone.now().date() > self.license_expiry
|
|
return False
|
|
|
|
def lock_account(self, duration_minutes=15):
|
|
"""
|
|
Lock the user account for specified duration.
|
|
"""
|
|
self.locked_until = timezone.now() + timezone.timedelta(minutes=duration_minutes)
|
|
self.save(update_fields=['locked_until'])
|
|
|
|
def unlock_account(self):
|
|
"""
|
|
Unlock the user account.
|
|
"""
|
|
self.locked_until = None
|
|
self.failed_login_attempts = 0
|
|
self.save(update_fields=['locked_until', 'failed_login_attempts'])
|
|
|
|
def increment_failed_login(self):
|
|
"""
|
|
Increment failed login attempts and lock if threshold reached.
|
|
"""
|
|
self.failed_login_attempts += 1
|
|
|
|
# Lock account after 5 failed attempts
|
|
max_attempts = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('MAX_LOGIN_ATTEMPTS', 5)
|
|
if self.failed_login_attempts >= max_attempts:
|
|
lockout_duration = getattr(settings, 'HOSPITAL_SETTINGS', {}).get('LOCKOUT_DURATION_MINUTES', 15)
|
|
self.lock_account(lockout_duration)
|
|
|
|
self.save(update_fields=['failed_login_attempts'])
|
|
|
|
def reset_failed_login(self):
|
|
"""
|
|
Reset failed login attempts.
|
|
"""
|
|
self.failed_login_attempts = 0
|
|
self.save(update_fields=['failed_login_attempts'])
|
|
|
|
|
|
class TwoFactorDevice(models.Model):
|
|
"""
|
|
Two-factor authentication devices for users.
|
|
"""
|
|
|
|
# User relationship
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='two_factor_devices'
|
|
)
|
|
|
|
# Device Information
|
|
device_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique device identifier'
|
|
)
|
|
name = models.CharField(
|
|
max_length=100,
|
|
help_text='Device name'
|
|
)
|
|
device_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('TOTP', 'Time-based OTP (Authenticator App)'),
|
|
('SMS', 'SMS'),
|
|
('EMAIL', 'Email'),
|
|
('HARDWARE', 'Hardware Token'),
|
|
('BACKUP', 'Backup Codes'),
|
|
]
|
|
)
|
|
|
|
# Device Configuration
|
|
secret_key = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Secret key for TOTP devices'
|
|
)
|
|
phone_number = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Phone number for SMS devices'
|
|
)
|
|
email_address = models.EmailField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Email address for email devices'
|
|
)
|
|
|
|
# Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Device is active'
|
|
)
|
|
is_verified = models.BooleanField(
|
|
default=False,
|
|
help_text='Device is verified'
|
|
)
|
|
verified_at = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Device verification date'
|
|
)
|
|
|
|
# Usage Statistics
|
|
last_used_at = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Last time device was used'
|
|
)
|
|
usage_count = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Number of times device was used'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'accounts_two_factor_device'
|
|
verbose_name = 'Two Factor Device'
|
|
verbose_name_plural = 'Two Factor Devices'
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} - {self.name} ({self.device_type})"
|
|
|
|
|
|
class SocialAccount(models.Model):
|
|
"""
|
|
Social authentication accounts linked to users.
|
|
"""
|
|
|
|
# User relationship
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='social_accounts'
|
|
)
|
|
|
|
# Provider Information
|
|
provider = models.CharField(
|
|
max_length=50,
|
|
choices=[
|
|
('GOOGLE', 'Google'),
|
|
('MICROSOFT', 'Microsoft'),
|
|
('APPLE', 'Apple'),
|
|
('FACEBOOK', 'Facebook'),
|
|
('LINKEDIN', 'LinkedIn'),
|
|
('GITHUB', 'GitHub'),
|
|
('OKTA', 'Okta'),
|
|
('SAML', 'SAML'),
|
|
('LDAP', 'LDAP'),
|
|
]
|
|
)
|
|
provider_id = models.CharField(
|
|
max_length=200,
|
|
help_text='Provider user ID'
|
|
)
|
|
provider_email = models.EmailField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Email from provider'
|
|
)
|
|
|
|
# Account Information
|
|
display_name = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Display name from provider'
|
|
)
|
|
profile_url = models.URLField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Profile URL from provider'
|
|
)
|
|
avatar_url = models.URLField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Avatar URL from provider'
|
|
)
|
|
|
|
# Tokens
|
|
access_token = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Access token from provider'
|
|
)
|
|
refresh_token = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Refresh token from provider'
|
|
)
|
|
token_expires_at = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Token expiration date'
|
|
)
|
|
|
|
# Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Social account is active'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
last_login_at = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Last login using this social account'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'accounts_social_account'
|
|
verbose_name = 'Social Account'
|
|
verbose_name_plural = 'Social Accounts'
|
|
unique_together = ['provider', 'provider_id']
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} - {self.provider}"
|
|
|
|
|
|
class UserSession(models.Model):
|
|
"""
|
|
User session tracking for security and audit purposes.
|
|
"""
|
|
|
|
# User relationship
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='user_sessions'
|
|
)
|
|
|
|
# Session Information
|
|
session_key = models.CharField(
|
|
max_length=40,
|
|
unique=True,
|
|
help_text='Django session key'
|
|
)
|
|
session_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique session identifier'
|
|
)
|
|
|
|
# Device Information
|
|
ip_address = models.GenericIPAddressField(
|
|
help_text='IP address'
|
|
)
|
|
user_agent = models.TextField(
|
|
help_text='User agent string'
|
|
)
|
|
device_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('DESKTOP', 'Desktop'),
|
|
('MOBILE', 'Mobile'),
|
|
('TABLET', 'Tablet'),
|
|
('UNKNOWN', 'Unknown'),
|
|
],
|
|
default='UNKNOWN'
|
|
)
|
|
browser = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Browser name and version'
|
|
)
|
|
operating_system = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Operating system'
|
|
)
|
|
|
|
# Location Information
|
|
country = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Country'
|
|
)
|
|
region = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Region/State'
|
|
)
|
|
city = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='City'
|
|
)
|
|
|
|
# Session Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Session is active'
|
|
)
|
|
login_method = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('PASSWORD', 'Password'),
|
|
('TWO_FACTOR', 'Two Factor'),
|
|
('SOCIAL', 'Social Login'),
|
|
('SSO', 'Single Sign-On'),
|
|
('API_KEY', 'API Key'),
|
|
],
|
|
default='PASSWORD'
|
|
)
|
|
|
|
# Timestamps
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
last_activity_at = models.DateTimeField(auto_now=True)
|
|
expires_at = models.DateTimeField(
|
|
help_text='Session expiration time'
|
|
)
|
|
ended_at = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Session end time'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'accounts_user_session'
|
|
verbose_name = 'User Session'
|
|
verbose_name_plural = 'User Sessions'
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['user', 'is_active']),
|
|
models.Index(fields=['session_key']),
|
|
models.Index(fields=['ip_address']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} - {self.ip_address} - {self.created_at}"
|
|
|
|
@property
|
|
def is_expired(self):
|
|
"""
|
|
Check if session has expired.
|
|
"""
|
|
return timezone.now() > self.expires_at
|
|
|
|
def end_session(self):
|
|
"""
|
|
End the session.
|
|
"""
|
|
self.is_active = False
|
|
self.ended_at = timezone.now()
|
|
self.save(update_fields=['is_active', 'ended_at'])
|
|
|
|
|
|
class PasswordHistory(models.Model):
|
|
"""
|
|
Password history for users to prevent password reuse.
|
|
"""
|
|
|
|
# User relationship
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='password_history'
|
|
)
|
|
|
|
# Password Information
|
|
password_hash = models.CharField(
|
|
max_length=128,
|
|
help_text='Hashed password'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = 'accounts_password_history'
|
|
verbose_name = 'Password History'
|
|
verbose_name_plural = 'Password History'
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.user.username} - {self.created_at}"
|
|
|