2025-08-12 13:33:25 +03:00

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}"