Files
smoothschedule/smoothschedule/config/settings/multitenancy.py
poduck a4b23e44b6 feat(messaging): Add broadcast messaging system for owners and managers
- 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>
2025-12-08 02:33:27 -05:00

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",
)