Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes: - Django backend with multi-tenancy (django-tenants) - React + TypeScript frontend with Vite - Platform administration API with role-based access control - Authentication system with token-based auth - Quick login dev tools for testing different user roles - CORS and CSRF configuration for local development - Docker development environment setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
213
smoothschedule/config/settings/multitenancy.py
Normal file
213
smoothschedule/config/settings/multitenancy.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
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
|
||||
'core', # Core models (Tenant, Domain, PermissionGrant)
|
||||
|
||||
# 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',
|
||||
|
||||
# Users app (shared across tenants)
|
||||
'smoothschedule.users',
|
||||
|
||||
# Third-party apps that should be shared
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'corsheaders',
|
||||
'drf_spectacular',
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'allauth.mfa',
|
||||
'allauth.socialaccount',
|
||||
'django_celery_beat',
|
||||
'hijack',
|
||||
'hijack.contrib.admin',
|
||||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
]
|
||||
|
||||
# Tenant-specific apps - Each tenant gets isolated data in their own schema
|
||||
TENANT_APPS = [
|
||||
'django.contrib.contenttypes', # Needed for tenant schemas
|
||||
'schedule', # Resource scheduling with configurable concurrency
|
||||
'payments', # Stripe Connect payments bridge
|
||||
'communication', # Twilio masked communications
|
||||
|
||||
# Add your tenant-scoped business logic apps here:
|
||||
# 'appointments',
|
||||
# 'customers',
|
||||
# 'analytics',
|
||||
]
|
||||
|
||||
# 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 = [
|
||||
# 1. MUST BE FIRST: Tenant resolution
|
||||
'django_tenants.middleware.main.TenantMainMiddleware',
|
||||
|
||||
# 2. Security middleware
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware', # Moved up for better CORS handling
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
|
||||
# 3. Session & CSRF
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'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
|
||||
'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 = '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)
|
||||
Reference in New Issue
Block a user