"""Fonrey base Django settings. Secrets via env (python-decouple). ASGI + django-tenants.""" from pathlib import Path from decouple import Csv, config as env BASE_DIR = Path(__file__).resolve().parent.parent.parent SECRET_KEY = env("SECRET_KEY") DEBUG = env("DEBUG", default=False, cast=bool) ALLOWED_HOSTS = env("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=Csv()) DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" SHARED_APPS = [ "django_tenants", # MUST be first per django-tenants "apps.tenant", "apps.release", "shared", "django.contrib.contenttypes", "django.contrib.auth", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django_celery_beat", "django_celery_results", "rest_framework", "drf_spectacular", "core", "django_htmx", "django_extensions", ] TENANT_APPS = [ "apps.account", "apps.permission", "apps.org", "apps.region", "apps.complex", "apps.property", "apps.client", "apps.setting", ] INSTALLED_APPS = list(SHARED_APPS) + [a for a in TENANT_APPS if a not in SHARED_APPS] TENANT_MODEL = "tenant.Tenant" TENANT_DOMAIN_MODEL = "tenant.Domain" MIDDLEWARE = [ "django_tenants.middleware.main.TenantMainMiddleware", # MUST be first "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django_htmx.middleware.HtmxMiddleware", "core.middleware.audit.AuditMiddleware", ] ROOT_URLCONF = "config.urls" PUBLIC_SCHEMA_URLCONF = "config.urls_public" DATABASES = { "default": { "ENGINE": "django_tenants.postgresql_backend", "NAME": env("DB_NAME"), "USER": env("DB_USER"), "PASSWORD": env("DB_PASSWORD"), "HOST": env("DB_HOST", default="localhost"), "PORT": env("DB_PORT", default="5432"), "CONN_MAX_AGE": 60, # Connection pooling lives at PgBouncer; no non-standard DSN keys here. } } DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"] CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": env("REDIS_URL", default="redis://127.0.0.1:6379/0"), "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, "KEY_PREFIX": "fonrey", } } SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://127.0.0.1:6379/1") CELERY_RESULT_BACKEND = "django-db" CELERY_TASK_ALWAYS_EAGER = False CELERY_TASK_TIME_LIMIT = 300 CELERY_TASK_SOFT_TIME_LIMIT = 270 CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" CELERY_ACCEPT_CONTENT = ["json"] CELERY_TASK_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json" CELERY_TIMEZONE = "Asia/Shanghai" DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" AWS_S3_ENDPOINT_URL = env("R2_ENDPOINT_URL", default="") AWS_ACCESS_KEY_ID = env("R2_ACCESS_KEY_ID", default="") AWS_SECRET_ACCESS_KEY = env("R2_SECRET_ACCESS_KEY", default="") AWS_STORAGE_BUCKET_NAME = env("R2_BUCKET_NAME", default="media") AWS_S3_CUSTOM_DOMAIN = env("R2_CUSTOM_DOMAIN", default="") or None AWS_DEFAULT_ACL = "private" AWS_S3_SIGNATURE_VERSION = "s3v4" AWS_S3_FILE_OVERWRITE = False ASGI_APPLICATION = "config.asgi.application" WSGI_APPLICATION = "config.wsgi.application" AUTH_USER_MODEL = "account.UserAccount" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] }, } ] HTMX_GLOBAL_CSRF = True STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" STATICFILES_DIRS = [BASE_DIR / "static"] MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" LANGUAGE_CODE = "zh-hans" TIME_ZONE = "Asia/Shanghai" USE_I18N = True USE_TZ = True LOCALE_PATHS = [BASE_DIR / "locale"] SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Lax" # CSRF_COOKIE_HTTPONLY MUST be False so HTMX can read the token from JS (spec §3.2). CSRF_COOKIE_HTTPONLY = False X_FRAME_OPTIONS = "DENY" REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.SessionAuthentication", ], "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", ], } SPECTACULAR_SETTINGS = { "TITLE": "Fonrey API", "DESCRIPTION": "Fonrey 房产经纪管理系统 OpenAPI 3.1", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, "COMPONENT_SPLIT_REQUEST": True, "ENUM_GENERATE_CHOICE_DESCRIPTION": True, } LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "[{asctime}] {levelname} {name} ({process:d}) {message}", "style": "{", }, }, "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "verbose", } }, "root": {"handlers": ["console"], "level": "INFO"}, } PHONE_ENCRYPTION_KEY = env("PHONE_ENCRYPTION_KEY", default="")