""" 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. """ ROLE_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'), ] THEME_CHOICES = [ ('LIGHT', 'Light'), ('DARK', 'Dark'), ('AUTO', 'Auto'), ] # 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=ROLE_CHOICES, 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=THEME_CHOICES, 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}"