- Add BroadcastMessage and MessageRecipient models for sending messages to groups or individuals - Add Messages page with compose form and sent messages list - Support targeting by role (owners, managers, staff, customers) or individual users - Add can_send_messages permission (owners always, managers by default with revocable permission) - Add autofill search dropdown with infinite scroll for selecting individual recipients - Add staff permission toggle for managers' messaging access - Integrate Messages link in sidebar for users with permission 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
280 lines
9.6 KiB
Python
280 lines
9.6 KiB
Python
"""
|
|
Smooth Schedule - Multi-Tenancy Settings Override
|
|
This file extends base.py and adds django-tenants configuration
|
|
"""
|
|
|
|
from .base import * # noqa
|
|
from .base import INSTALLED_APPS, MIDDLEWARE, DATABASES, LOGGING, env
|
|
|
|
# =============================================================================
|
|
# MULTI-TENANCY CONFIGURATION (django-tenants)
|
|
# =============================================================================
|
|
|
|
# Shared apps - Available to all tenants (stored in 'public' schema)
|
|
SHARED_APPS = [
|
|
'django_tenants', # Must be first
|
|
|
|
# Identity Domain (shared)
|
|
'smoothschedule.identity.core', # Core models (Tenant, Domain, PermissionGrant)
|
|
'smoothschedule.identity.users', # Users app (shared across tenants)
|
|
|
|
# Platform Domain (shared)
|
|
'smoothschedule.platform.admin', # Platform management (TenantInvitation, etc.)
|
|
'smoothschedule.platform.api', # Public API v1 for third-party integrations
|
|
|
|
# Django built-ins (must be in shared)
|
|
'django.contrib.contenttypes',
|
|
'django.contrib.auth',
|
|
'django.contrib.sessions',
|
|
'django.contrib.sites',
|
|
'django.contrib.messages',
|
|
'django.contrib.staticfiles',
|
|
'django.contrib.admin',
|
|
|
|
# Third-party apps that should be shared
|
|
'rest_framework',
|
|
'rest_framework.authtoken',
|
|
'corsheaders',
|
|
'drf_spectacular',
|
|
'channels', # WebSockets
|
|
'allauth',
|
|
'allauth.account',
|
|
'allauth.mfa',
|
|
'allauth.socialaccount',
|
|
'django_celery_beat',
|
|
'hijack',
|
|
'hijack.contrib.admin',
|
|
'crispy_forms',
|
|
'crispy_bootstrap5',
|
|
'csp',
|
|
'djstripe', # Stripe integration
|
|
|
|
# Commerce Domain (shared for platform support)
|
|
'smoothschedule.commerce.tickets', # Ticket system - shared for platform support access
|
|
|
|
# Communication Domain (shared)
|
|
'smoothschedule.communication.notifications', # Notification system - shared for platform
|
|
'smoothschedule.communication.credits', # Communication credits (SMS/calling) - shared for billing
|
|
'smoothschedule.communication.mobile', # Field employee mobile app - shared for location tracking
|
|
]
|
|
|
|
# Tenant-specific apps - Each tenant gets isolated data in their own schema
|
|
TENANT_APPS = [
|
|
'django.contrib.contenttypes', # Needed for tenant schemas
|
|
|
|
# Scheduling Domain (tenant-isolated)
|
|
'smoothschedule.scheduling.schedule', # Resource scheduling with configurable concurrency
|
|
'smoothschedule.scheduling.contracts', # Contract/e-signature system
|
|
|
|
# Communication Domain (tenant-isolated)
|
|
'smoothschedule.communication.messaging', # Broadcast messaging system
|
|
|
|
# Commerce Domain (tenant-isolated)
|
|
'smoothschedule.commerce.payments', # Stripe Connect payments bridge
|
|
]
|
|
|
|
|
|
# Override INSTALLED_APPS to include all unique apps
|
|
INSTALLED_APPS = list(dict.fromkeys(SHARED_APPS + TENANT_APPS))
|
|
|
|
# Tenant model configuration
|
|
TENANT_MODEL = "core.Tenant"
|
|
TENANT_DOMAIN_MODEL = "core.Domain"
|
|
PUBLIC_SCHEMA_NAME = 'public'
|
|
|
|
# =============================================================================
|
|
# DATABASE CONFIGURATION (Multi-schema)
|
|
# =============================================================================
|
|
|
|
# Override database engine for django-tenants
|
|
DATABASES['default']['ENGINE'] ='django_tenants.postgresql_backend'
|
|
|
|
# Database routers for tenant isolation
|
|
DATABASE_ROUTERS = [
|
|
'django_tenants.routers.TenantSyncRouter',
|
|
]
|
|
|
|
# =============================================================================
|
|
# MIDDLEWARE CONFIGURATION
|
|
# =============================================================================
|
|
# CRITICAL: Order matters!
|
|
|
|
MIDDLEWARE = [
|
|
# 0. CORS must be first to set headers before tenant resolution
|
|
'corsheaders.middleware.CorsMiddleware',
|
|
|
|
# 1. Tenant resolution
|
|
'django_tenants.middleware.main.TenantMainMiddleware',
|
|
'smoothschedule.identity.core.middleware.TenantHeaderMiddleware', # Support tenant switching via header
|
|
|
|
# 2. Security middleware
|
|
'django.middleware.security.SecurityMiddleware',
|
|
'csp.middleware.CSPMiddleware',
|
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
|
|
|
# 3. Session & CSRF
|
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
|
|
# 4. Sandbox mode - switches to sandbox schema if requested
|
|
# MUST come after TenantMainMiddleware and SessionMiddleware
|
|
'smoothschedule.identity.core.middleware.SandboxModeMiddleware',
|
|
'django.middleware.locale.LocaleMiddleware',
|
|
'django.middleware.common.CommonMiddleware',
|
|
'django.middleware.csrf.CsrfViewMiddleware',
|
|
|
|
# 4. Authentication
|
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
|
|
# 5. Hijack (Masquerading) - MUST come before our audit middleware
|
|
'hijack.middleware.HijackUserMiddleware',
|
|
|
|
# 6. MASQUERADE AUDIT - MUST come AFTER HijackUserMiddleware
|
|
'smoothschedule.identity.core.middleware.MasqueradeAuditMiddleware',
|
|
|
|
# 7. Messages, Clickjacking, and Allauth
|
|
'django.contrib.messages.middleware.MessageMiddleware',
|
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
'allauth.account.middleware.AccountMiddleware',
|
|
]
|
|
|
|
# =============================================================================
|
|
# TEMPLATE CONTEXT PROCESSORS (for admin)
|
|
# =============================================================================
|
|
|
|
# Ensure required context processors are present
|
|
from .base import TEMPLATES # noqa
|
|
if TEMPLATES and len(TEMPLATES) > 0:
|
|
context_processors = TEMPLATES[0]['OPTIONS']['context_processors']
|
|
if 'django.contrib.auth.context_processors.auth' not in context_processors:
|
|
context_processors.insert(0, 'django.contrib.auth.context_processors.auth')
|
|
if 'django.contrib.messages.context_processors.messages' not in context_processors:
|
|
context_processors.append('django.contrib.messages.context_processors.messages')
|
|
|
|
# =============================================================================
|
|
# PASSWORD VALIDATION
|
|
# =============================================================================
|
|
|
|
# Password hashers
|
|
PASSWORD_HASHERS = [
|
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
|
]
|
|
|
|
# Password validators
|
|
AUTH_PASSWORD_VALIDATORS = [
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
'OPTIONS': {
|
|
'min_length': 10,
|
|
}
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
},
|
|
]
|
|
|
|
# =============================================================================
|
|
# HIJACK (MASQUERADING) CONFIGURATION
|
|
# =============================================================================
|
|
|
|
HIJACK_AUTHORIZATION_CHECK = 'smoothschedule.identity.core.permissions.can_hijack'
|
|
HIJACK_DISPLAY_ADMIN_BUTTON = True
|
|
HIJACK_USE_BOOTSTRAP = True
|
|
HIJACK_ALLOW_GET_REQUESTS = False # Security: require POST
|
|
HIJACK_INSERT_BEFORE = True
|
|
|
|
# =============================================================================
|
|
# ENHANCED LOGGING FOR SECURITY AUDIT
|
|
# =============================================================================
|
|
|
|
# Extend existing logging configuration
|
|
LOGGING['formatters']['json'] = {
|
|
'()': 'django.utils.log.ServerFormatter',
|
|
'format': '[{server_time}] {message}',
|
|
'style': '{',
|
|
}
|
|
|
|
LOGGING['handlers']['security_file'] = {
|
|
'class': 'logging.handlers.RotatingFileHandler',
|
|
'filename': str(BASE_DIR / 'logs' / 'security.log'),
|
|
'maxBytes': 1024 * 1024 * 10, # 10 MB
|
|
'backupCount': 5,
|
|
'formatter': 'json',
|
|
}
|
|
|
|
LOGGING['handlers']['masquerade_file'] = {
|
|
'class': 'logging.handlers.RotatingFileHandler',
|
|
'filename': str(BASE_DIR / 'logs' / 'masquerade.log'),
|
|
'maxBytes': 1024 * 1024 * 10, # 10 MB
|
|
'backupCount': 5,
|
|
'formatter': 'json',
|
|
}
|
|
|
|
# Ensure 'loggers' key exists in LOGGING
|
|
if 'loggers' not in LOGGING:
|
|
LOGGING['loggers'] = {}
|
|
|
|
LOGGING['loggers']['smoothschedule.security'] = {
|
|
'handlers': ['console', 'security_file'],
|
|
'level': 'INFO',
|
|
'propagate': False,
|
|
}
|
|
|
|
LOGGING['loggers']['smoothschedule.security.masquerade'] = {
|
|
'handlers': ['console', 'masquerade_file'],
|
|
'level': 'INFO',
|
|
'propagate': False,
|
|
}
|
|
|
|
# Create logs directory if it doesn't exist
|
|
import os
|
|
os.makedirs(BASE_DIR / 'logs', exist_ok=True)
|
|
|
|
# =============================================================================
|
|
# CONTENT SECURITY POLICY (CSP)
|
|
# =============================================================================
|
|
# https://django-csp.readthedocs.io/en/latest/configuration.html
|
|
|
|
CSP_DEFAULT_SRC = ("'self'",)
|
|
CSP_SCRIPT_SRC = (
|
|
"'self'",
|
|
"https://js.stripe.com",
|
|
"https://connect-js.stripe.com",
|
|
"https://www.googletagmanager.com",
|
|
"https://www.google-analytics.com",
|
|
"https://cdn.jsdelivr.net", # Required for Swagger UI
|
|
"blob:", # Required for Stripe
|
|
)
|
|
CSP_STYLE_SRC = (
|
|
"'self'",
|
|
"'unsafe-inline'", # Required for Stripe and many UI libraries
|
|
"https://cdn.jsdelivr.net", # Required for Swagger UI
|
|
)
|
|
CSP_IMG_SRC = (
|
|
"'self'",
|
|
"data:",
|
|
"https://*.stripe.com",
|
|
"https://www.google-analytics.com",
|
|
"https://cdn.jsdelivr.net", # Required for Swagger UI
|
|
)
|
|
CSP_CONNECT_SRC = (
|
|
"'self'",
|
|
"https://api.stripe.com",
|
|
"https://www.google-analytics.com",
|
|
"https://stats.g.doubleclick.net",
|
|
)
|
|
CSP_FRAME_SRC = (
|
|
"'self'",
|
|
"https://js.stripe.com",
|
|
"https://hooks.stripe.com",
|
|
"https://connect-js.stripe.com",
|
|
)
|