Skip to content
Snippets Groups Projects
settings.py 38.96 KiB
import os
import warnings
from copy import deepcopy
from glob import glob
from socket import getfqdn

from django.utils.log import DEFAULT_LOGGING
from django.utils.translation import gettext_lazy as _

from dynaconf import LazySettings

from .util.core_helpers import (
    get_app_packages,
    get_app_settings_overrides,
    merge_app_settings,
    monkey_patch,
)

monkey_patch()

IN_PYTEST = "PYTEST_CURRENT_TEST" in os.environ or "TOX_ENV_DIR" in os.environ
PYTEST_SETUP_DATABASES = [("default", "default_oot")]

ENVVAR_PREFIX_FOR_DYNACONF = "ALEKSIS"
DIRS_FOR_DYNACONF = ["/etc/aleksis"]
MERGE_ENABLED_FOR_DYNACONF = True

SETTINGS_FILE_FOR_DYNACONF = []
for directory in DIRS_FOR_DYNACONF:
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*.json"))
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*.ini"))
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*.yaml"))
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*.toml"))
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.json"))
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.ini"))
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.yaml"))
    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.toml"))

_settings = LazySettings(
    ENVVAR_PREFIX_FOR_DYNACONF=ENVVAR_PREFIX_FOR_DYNACONF,
    SETTINGS_FILE_FOR_DYNACONF=SETTINGS_FILE_FOR_DYNACONF,
    MERGE_ENABLED_FOR_DYNACONF=MERGE_ENABLED_FOR_DYNACONF,
)

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Cache directory for external operations
CACHE_DIR = _settings.get("caching.dir", os.path.join(BASE_DIR, "cache"))

SILENCED_SYSTEM_CHECKS = []

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = _settings.get("secret_key", "DoNotUseInProduction")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = _settings.get("maintenance.debug", False)
INTERNAL_IPS = _settings.get("maintenance.internal_ips", [])

UWSGI = {
    "module": "aleksis.core.wsgi",
}
UWSGI_SERVE_STATIC = True
UWSGI_SERVE_MEDIA = False

DEV_SERVER_PORT = 8000
DJANGO_VITE_DEV_SERVER_PORT = DEV_SERVER_PORT + 1

ALLOWED_HOSTS = _settings.get("http.allowed_hosts", [getfqdn(), "localhost", "127.0.0.1", "[::1]"])
BASE_URL = _settings.get(
    "http.base_url",
    f"http://localhost:{DEV_SERVER_PORT}" if DEBUG else f"https://{ALLOWED_HOSTS[0]}",
)


def generate_trusted_origins():
    origins = []
    origins += [f"http://{host}" for host in ALLOWED_HOSTS]
    origins += [f"https://{host}" for host in ALLOWED_HOSTS]
    if DEBUG:
        origins += [f"http://{host}:{DEV_SERVER_PORT}" for host in ALLOWED_HOSTS]
        origins += [f"http://{host}:{DJANGO_VITE_DEV_SERVER_PORT}" for host in ALLOWED_HOSTS]
    return origins


CSRF_TRUSTED_ORIGINS = _settings.get("http.trusted_origins", generate_trusted_origins())

# Application definition
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.sites",
    "django.contrib.staticfiles",
    "django.contrib.humanize",
    "django_uwsgi",
    "django_extensions",
    "guardian",
    "rules.apps.AutodiscoverRulesConfig",
    "haystack",
    "polymorphic",
    "dj_cleavejs.apps.DjCleaveJSConfig",
    "dbbackup",
    "django_celery_beat",
    "django_celery_results",
    "celery_progress",
    "health_check.contrib.celery",
    "djcelery_email",
    "celery_haystack",
    "sass_processor",
    "django_any_js",
    "django_yarnpkg",
    "django_vite",
    "django_tables2",
    "maintenance_mode",
    "reversion",
    "phonenumber_field",
    "django_prometheus",
    "django_select2",
    "templated_email",
    "html2text",
    "django_otp.plugins.otp_totp",
    "django_otp.plugins.otp_static",
    "django_otp.plugins.otp_email",
    "django_otp",
    "otp_yubikey",
    "aleksis.core",
    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "invitations",
    "health_check",
    "health_check.db",
    "health_check.cache",
    "health_check.storage",
    "health_check.contrib.psutil",
    "health_check.contrib.migrations",
    "dynamic_preferences",
    "dynamic_preferences.users.apps.UserPreferencesConfig",
    "impersonate",
    "two_factor",
    "two_factor.plugins.email",
    "two_factor.plugins.phonenumber",
    "two_factor.plugins.yubikey",
    "two_factor.plugins.webauthn",
    "material",
    "ckeditor",
    "ckeditor_uploader",
    "colorfield",
    "django_bleach",
    "favicon",
    "django_filters",
    "oauth2_provider",
    "rest_framework",
    "graphene_django",
    "dj_iconify.apps.DjIconifyConfig",
    "recurrence",
]

merge_app_settings("INSTALLED_APPS", INSTALLED_APPS, True)
INSTALLED_APPS += get_app_packages()

STATICFILES_FINDERS = [
    "django.contrib.staticfiles.finders.FileSystemFinder",
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
    "sass_processor.finders.CssFinder",
]

MIDDLEWARE = [
    #    'django.middleware.cache.UpdateCacheMiddleware',
    "django_prometheus.middleware.PrometheusBeforeMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.http.ConditionalGetMiddleware",
    "django.contrib.sites.middleware.CurrentSiteMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django_otp.middleware.OTPMiddleware",
    "impersonate.middleware.ImpersonateMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "maintenance_mode.middleware.MaintenanceModeMiddleware",
    "aleksis.core.util.middlewares.EnsurePersonMiddleware",
    "django_prometheus.middleware.PrometheusAfterMiddleware",
    "allauth.account.middleware.AccountMiddleware",
    #    'django.middleware.cache.FetchFromCacheMiddleware'
]

ROOT_URLCONF = "aleksis.core.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "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",
                "maintenance_mode.context_processors.maintenance_mode",
                "dynamic_preferences.processors.global_preferences",
                "aleksis.core.util.core_helpers.custom_information_processor",
                "aleksis.core.util.context_processors.need_maintenance_response_context_processor",
            ],
        },
    },
]

# Attention: The following context processors must accept None
# as first argument (in addition to a HttpRequest object)
NON_REQUEST_CONTEXT_PROCESSORS = [
    "django.template.context_processors.i18n",
    "django.template.context_processors.tz",
    "aleksis.core.util.core_helpers.custom_information_processor",
]

WSGI_APPLICATION = "aleksis.core.wsgi.application"

# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django_prometheus.db.backends.postgresql",
        "NAME": _settings.get("database.name", "aleksis"),
        "USER": _settings.get("database.username", "aleksis"),
        "PASSWORD": _settings.get("database.password", None),
        "HOST": _settings.get("database.host", "127.0.0.1"),
        "PORT": _settings.get("database.port", "5432"),
        "CONN_MAX_AGE": _settings.get("database.conn_max_age", None),
        "CONN_HEALTH_CHECK": True,
        "OPTIONS": _settings.get("database.options", {}),
    }
}

# Duplicate default database for out-of-transaction updates
DATABASES["default_oot"] = DATABASES["default"].copy()
DATABASE_ROUTERS = [
    "aleksis.core.util.core_helpers.OOTRouter",
]
DATABASE_OOT_LABELS = ["django_celery_results"]

merge_app_settings("DATABASES", DATABASES, False)

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.ScryptPasswordHasher",
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
]

REDIS_HOST = _settings.get("redis.host", "localhost")
REDIS_PORT = _settings.get("redis.port", 6379)
REDIS_DB = _settings.get("redis.database", 0)
REDIS_PASSWORD = _settings.get("redis.password", None)
REDIS_USER = _settings.get("redis.user", None if REDIS_PASSWORD is None else "default")

REDIS_URL = (
    f"redis://{REDIS_USER+':'+REDIS_PASSWORD+'@' if REDIS_USER else ''}"
    f"{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}"
)

if _settings.get("caching.redis.enabled", not IN_PYTEST):
    CACHES = {
        "default": {
            "BACKEND": "django.core.cache.backends.redis.RedisCache",
            "LOCATION": _settings.get("caching.redis.address", REDIS_URL),
        }
    }
else:
    CACHES = {
        "default": {
            # Use uWSGI if available (will auot-fallback to LocMemCache)
            "BACKEND": "django_uwsgi.cache.UwsgiCache"
        }
    }

INSTALLED_APPS.append("cachalot")
CACHALOT_TIMEOUT = _settings.get("caching.cachalot.timeout", None)
CACHALOT_DATABASES = set(["default", "default_oot"])
SILENCED_SYSTEM_CHECKS += ["cachalot.W001"]
CACHALOT_ENABLED = _settings.get("caching.query_caching", True)
CACHALOT_UNCACHABLE_TABLES = ("django_migrations", "django_session")

SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
SESSION_CACHE_ALIAS = "default"

# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]

AUTH_INITIAL_SUPERUSER = {
    "username": _settings.get("auth.superuser.username", "admin"),
    "password": _settings.get("auth.superuser.password", "admin"),
    "email": _settings.get("auth.superuser.email", "root@example.com"),
}

# Authentication backends are dynamically populated
AUTHENTICATION_BACKENDS = []

# Configuration for django-allauth.

# Use custom adapter to override some behaviour, i.e. honour the LDAP backend
SOCIALACCOUNT_ADAPTER = "aleksis.core.util.auth_helpers.OurSocialAccountAdapter"

# Get django-allauth providers from config
_SOCIALACCOUNT_PROVIDERS = _settings.get("auth.providers", None)
if _SOCIALACCOUNT_PROVIDERS:
    SOCIALACCOUNT_PROVIDERS = _SOCIALACCOUNT_PROVIDERS.to_dict()

    # Add configured social auth providers to INSTALLED_APPS
    for provider, config in SOCIALACCOUNT_PROVIDERS.items():
        INSTALLED_APPS.append(f"allauth.socialaccount.providers.{provider}")
        SOCIALACCOUNT_PROVIDERS[provider] = {k.upper(): v for k, v in config.items()}


# Configure custom forms

ACCOUNT_FORMS = {
    "signup": "aleksis.core.forms.AccountRegisterForm",
}

# Use custom adapter
ACCOUNT_ADAPTER = "aleksis.core.util.auth_helpers.OurAccountAdapter"

# Require password confirmation
SIGNUP_PASSWORD_ENTER_TWICE = True

# Allow login by either username or email
ACCOUNT_AUTHENTICATION_METHOD = _settings.get("auth.registration.method", "username_email")

# Require email address to sign up
ACCOUNT_EMAIL_REQUIRED = _settings.get("auth.registration.email_required", True)
SOCIALACCOUNT_EMAIL_REQUIRED = False

# Cooldown for verification mails
ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN = _settings.get("auth.registration.verification_cooldown", 180)

# Require email verification after sign up
ACCOUNT_EMAIL_VERIFICATION = _settings.get("auth.registration.email_verification", "optional")
SOCIALACCOUNT_EMAIL_VERIFICATION = False

# Email subject prefix for verification mails
ACCOUNT_EMAIL_SUBJECT_PREFIX = _settings.get("auth.registration.subject", "[AlekSIS] ")

# Max attempts before login timeout
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = _settings.get("auth.login.login_limit", 5)

# Login timeout after max attempts in seconds
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = _settings.get("auth.login.login_timeout", 300)

# Email confirmation field in form
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True

# Enforce uniqueness of email addresses
ACCOUNT_UNIQUE_EMAIL = _settings.get("auth.login.registration.unique_email", True)

# Configurable username validators
ACCOUNT_USERNAME_VALIDATORS = "aleksis.core.util.auth_helpers.custom_username_validators"

# Configuration for django-invitations

# Use custom account adapter
ACCOUNT_ADAPTER = "invitations.models.InvitationsAdapter"
# Expire invitations are configured amout of days
INVITATIONS_INVITATION_EXPIRY = _settings.get("auth.invitation.expiry", 3)
# Use email prefix configured for django-allauth
INVITATIONS_EMAIL_SUBJECT_PREFIX = ACCOUNT_EMAIL_SUBJECT_PREFIX
# Use custom invitation model
INVITATIONS_INVITATION_MODEL = "core.PersonInvitation"
# Use custom invitation form
INVITATIONS_INVITE_FORM = "aleksis.core.forms.PersonCreateInviteForm"
# Display error message if invitation code is invalid
INVITATIONS_GONE_ON_ACCEPT_ERROR = False
# Mark invitation as accepted after signup
INVITATIONS_ACCEPT_INVITE_AFTER_SIGNUP = True

# Configuration for OAuth2 provider
OAUTH2_PROVIDER = {
    "SCOPES_BACKEND_CLASS": "aleksis.core.util.auth_helpers.AppScopes",
    "OAUTH2_VALIDATOR_CLASS": "aleksis.core.util.auth_helpers.CustomOAuth2Validator",
    "OIDC_ENABLED": True,
    "OIDC_ISS_ENDPOINT": BASE_URL,
    "REFRESH_TOKEN_EXPIRE_SECONDS": _settings.get("oauth2.token_expiry", 86400),
    "PKCE_REQUIRED": False,
}
OAUTH2_PROVIDER_APPLICATION_MODEL = "core.OAuthApplication"
OAUTH2_PROVIDER_GRANT_MODEL = "core.OAuthGrant"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "core.OAuthAccessToken"  # noqa: S105
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "core.OAuthIDToken"  # noqa: S105
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "core.OAuthRefreshToken"  # noqa: S105

_OIDC_RSA_KEY_DEFAULT = "/etc/aleksis/oidc.pem"
_OIDC_RSA_KEY = _settings.get("oauth2.oidc.rsa_key", "/etc/aleksis/oidc.pem")
if "BEGIN RSA PRIVATE KEY" in _OIDC_RSA_KEY:
    OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"] = _OIDC_RSA_KEY
elif _OIDC_RSA_KEY == _OIDC_RSA_KEY_DEFAULT and not os.path.exists(_OIDC_RSA_KEY):
    warnings.warn(
        (
            f"The default OIDC RSA key in {_OIDC_RSA_KEY} does not exist. "
            f"RSA will be disabled for now, but creating and configuring a "
            f"key is recommended. To silence this warning, set oauth2.oidc.rsa_key "
            f"to the empty string in a configuration file."
        )
    )
elif _OIDC_RSA_KEY:
    with open(_OIDC_RSA_KEY, "r") as f:
        OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"] = f.read()

# Configuration for REST framework
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "oauth2_provider.contrib.rest_framework.OAuth2Authentication",
    ]
}

# Configuration for GraphQL framework
GRAPHENE = {
    "SCHEMA": "aleksis.core.schema.schema",
}

# LDAP config
if _settings.get("ldap.uri", None):
    # LDAP dependencies are not necessarily installed, so import them here
    import ldap  # noqa
    from django_auth_ldap.config import (
        LDAPSearch,
        LDAPSearchUnion,
        NestedGroupOfNamesType,
        NestedGroupOfUniqueNamesType,
        PosixGroupType,
    )

    AUTH_LDAP_GLOBAL_OPTIONS = {
        ldap.OPT_NETWORK_TIMEOUT: _settings.get("ldap.network_timeout", 3),
    }

    # Enable Django's integration to LDAP
    AUTHENTICATION_BACKENDS.append("aleksis.core.util.ldap.LDAPBackend")

    AUTH_LDAP_SERVER_URI = _settings.get("ldap.uri")

    # Optional: non-anonymous bind
    if _settings.get("ldap.bind.dn", None):
        AUTH_LDAP_BIND_DN = _settings.get("ldap.bind.dn")
        AUTH_LDAP_BIND_PASSWORD = _settings.get("ldap.bind.password")

    # Keep local password for users to be required to provide their old password on change
    AUTH_LDAP_SET_USABLE_PASSWORD = _settings.get("ldap.handle_passwords", True)

    # Keep bound as the authenticating user
    # Ensures proper read permissions, and ability to change password without admin
    AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True

    # The TOML config might contain either one table or an array of tables
    _AUTH_LDAP_USER_SETTINGS = _settings.get("ldap.users.search")
    if not isinstance(_AUTH_LDAP_USER_SETTINGS, list):
        _AUTH_LDAP_USER_SETTINGS = [_AUTH_LDAP_USER_SETTINGS]

    # Search attributes to find users by username
    AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
        *[
            LDAPSearch(
                entry["base"],
                ldap.SCOPE_SUBTREE,
                entry.get("filter", "(uid=%(user)s)"),
            )
            for entry in _AUTH_LDAP_USER_SETTINGS
        ]
    )

    # Mapping of LDAP attributes to Django model fields
    AUTH_LDAP_USER_ATTR_MAP = {
        "first_name": _settings.get("ldap.users.map.first_name", "givenName"),
        "last_name": _settings.get("ldap.users.map.last_name", "sn"),
        "email": _settings.get("ldap.users.map.email", "mail"),
    }

    # Discover flags by LDAP groups
    if _settings.get("ldap.groups.search", None):
        group_type = _settings.get("ldap.groups.type", "groupOfNames")

        # The TOML config might contain either one table or an array of tables
        _AUTH_LDAP_GROUP_SETTINGS = _settings.get("ldap.groups.search")
        if not isinstance(_AUTH_LDAP_GROUP_SETTINGS, list):
            _AUTH_LDAP_GROUP_SETTINGS = [_AUTH_LDAP_GROUP_SETTINGS]

        AUTH_LDAP_GROUP_SEARCH = LDAPSearchUnion(
            *[
                LDAPSearch(
                    entry["base"],
                    ldap.SCOPE_SUBTREE,
                    entry.get("filter", f"(objectClass={group_type})"),
                )
                for entry in _AUTH_LDAP_GROUP_SETTINGS
            ]
        )

        _group_type = _settings.get("ldap.groups.type", "groupOfNames").lower()
        if _group_type == "groupofnames":
            AUTH_LDAP_GROUP_TYPE = NestedGroupOfNamesType()
        elif _group_type == "groupofuniquenames":
            AUTH_LDAP_GROUP_TYPE = NestedGroupOfUniqueNamesType()
        elif _group_type == "posixgroup":
            AUTH_LDAP_GROUP_TYPE = PosixGroupType()

        AUTH_LDAP_USER_FLAGS_BY_GROUP = {}
        for _flag in ["is_active", "is_staff", "is_superuser"]:
            _dn = _settings.get(f"ldap.groups.flags.{_flag}", None)
            if _dn:
                AUTH_LDAP_USER_FLAGS_BY_GROUP[_flag] = _dn

        # Backend admin requires superusers to also be staff members
        if (
            "is_superuser" in AUTH_LDAP_USER_FLAGS_BY_GROUP
            and "is_staff" not in AUTH_LDAP_USER_FLAGS_BY_GROUP
        ):
            AUTH_LDAP_USER_FLAGS_BY_GROUP["is_staff"] = AUTH_LDAP_USER_FLAGS_BY_GROUP[
                "is_superuser"
            ]

# Add ModelBackend last so all other backends get a chance
# to verify passwords first
AUTHENTICATION_BACKENDS.append("django.contrib.auth.backends.ModelBackend")

# Authentication backend for django-allauth.
AUTHENTICATION_BACKENDS.append("allauth.account.auth_backends.AuthenticationBackend")

# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

LANGUAGES = [
    ("en", _("English")),
    ("de", _("German")),
    ("uk", _("Ukrainian")),
]
LANGUAGE_CODE = _settings.get("l10n.lang", "en")
TIME_ZONE = _settings.get("l10n.tz", "UTC")
USE_TZ = True

PHONENUMBER_DEFAULT_REGION = _settings.get("l10n.phone_number_country", None)

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/


STATIC_URL = _settings.get("static.url", "/static/")
MEDIA_URL = _settings.get("media.url", "/media/")

LOGIN_REDIRECT_URL = "index"
LOGOUT_REDIRECT_URL = "index"

STATIC_ROOT = _settings.get("static.root", os.path.join(BASE_DIR, "static"))
MEDIA_ROOT = _settings.get("media.root", os.path.join(BASE_DIR, "media"))
NODE_MODULES_ROOT = CACHE_DIR

YARN_INSTALLED_APPS = [
    "cleave.js@^1.6.0",
    "@fontsource/roboto@^4.5.5",
    "jquery@^3.6.0",
    "@materializecss/materialize@~1.0.0",
    "material-design-icons-iconfont@^6.7.0",
    "select2-materialize@^0.1.8",
    "paper-css@^0.4.1",
    "jquery-sortablejs@^1.0.1",
    "sortablejs@^1.15.0",
    "@sentry/tracing@^7.28.0",
    "luxon@^2.3.2",
    "@iconify/iconify@^2.2.1",
    "@iconify/json@^2.1.30",
    "@mdi/font@^7.2.96",
    "apollo-boost@^0.4.9",
    "apollo-link-batch-http@^1.2.14",
    "apollo-link-retry@^2.2.16",
    "apollo3-cache-persist@^0.14.1",
    "deepmerge@^4.2.2",
    "graphql@^15.8.0",
    "graphql-tag@^2.12.6",
    "sass@^1.32",
    "vue@^2.7.7",
    "vue-apollo@^3.1.0",
    "vuetify@^2.6.7",
    "vue-router@^3.5.2",
    "vue-cookies@^1.8.2",
    "vite@^4.0.1",
    "vite-plugin-pwa@^0.14.1",
    "vite-plugin-top-level-await@^1.2.2",
    "@vitejs/plugin-vue2@^2.2.0",
    "@rollup/plugin-node-resolve@^15.0.1",
    "@rollup/plugin-graphql@^2.0.2",
    "@rollup/plugin-virtual@^3.0.1",
    "rollup-plugin-license@^3.0.1",
    "vue-i18n@^8.0.0",
    "browserslist-to-esbuild@^1.2.0",
    "@sentry/vue@^7.28.0",
    "vue-draggable-grid@^0.4.0",
    "rrule",
    "luxon@^3.4.3",
]

merge_app_settings("YARN_INSTALLED_APPS", YARN_INSTALLED_APPS, True)

JS_URL = _settings.get("js_assets.url", STATIC_URL)
JS_ROOT = _settings.get("js_assets.root", os.path.join(NODE_MODULES_ROOT, "node_modules"))

DJANGO_VITE_ASSETS_PATH = os.path.join(NODE_MODULES_ROOT, "vite_bundles")
DJANGO_VITE_DEV_MODE = DEBUG

STATICFILES_DIRS = (
    DJANGO_VITE_ASSETS_PATH,
    JS_ROOT,
)

ANY_JS = {
    "materialize": {"js_url": JS_URL + "/@materializecss/materialize/dist/js/materialize.min.js"},
    "jQuery": {"js_url": JS_URL + "/jquery/dist/jquery.min.js"},
    "material-design-icons": {
        "css_url": JS_URL + "/material-design-icons-iconfont/dist/material-design-icons.css"
    },
    "paper-css": {"css_url": JS_URL + "/paper-css/paper.min.css"},
    "select2-materialize": {
        "css_url": JS_URL + "/select2-materialize/select2-materialize.css",
        "js_url": JS_URL + "/select2-materialize/index.js",
    },
    "sortablejs": {"js_url": JS_URL + "/sortablejs/Sortable.min.js"},
    "jquery-sortablejs": {"js_url": JS_URL + "/jquery-sortablejs/jquery-sortable.js"},
    "Roboto100": {"css_url": JS_URL + "/@fontsource/roboto/100.css"},
    "Roboto300": {"css_url": JS_URL + "/@fontsource/roboto/300.css"},
    "Roboto400": {"css_url": JS_URL + "/@fontsource/roboto/400.css"},
    "Roboto500": {"css_url": JS_URL + "/@fontsource/roboto/500.css"},
    "Roboto700": {"css_url": JS_URL + "/@fontsource/roboto/700.css"},
    "Roboto900": {"css_url": JS_URL + "/@fontsource/roboto/900.css"},
    "Sentry": {"js_url": JS_URL + "/@sentry/tracing/build/bundle.tracing.js"},
    "cleavejs": {"js_url": JS_URL + "/cleave.js/dist/cleave.min.js"},
    "luxon": {"js_url": JS_URL + "/luxon/build/global/luxon.min.js"},
    "iconify": {"js_url": JS_URL + "/@iconify/iconify/dist/iconify.min.js"},
}

merge_app_settings("ANY_JS", ANY_JS, True)

CLEAVE_JS = ANY_JS["cleavejs"]["js_url"]

SASS_PROCESSOR_ENABLED = True
SASS_PROCESSOR_AUTO_INCLUDE = False
SASS_PROCESSOR_CUSTOM_FUNCTIONS = {
    "get-colour": "aleksis.core.util.sass_helpers.get_colour",
    "get-preference": "aleksis.core.util.sass_helpers.get_preference",
}
SASS_PROCESSOR_INCLUDE_DIRS = [
    _settings.get(
        "materialize.sass_path", os.path.join(JS_ROOT, "@materializecss", "materialize", "sass")
    ),
    os.path.join(STATIC_ROOT, "public"),
]

ICONIFY_JSON_ROOT = os.path.join(JS_ROOT, "@iconify", "json")
ICONIFY_COLLECTIONS_ALLOWED = ["mdi"]

ADMINS = _settings.get(
    "contact.admins", [(AUTH_INITIAL_SUPERUSER["username"], AUTH_INITIAL_SUPERUSER["email"])]
)
SERVER_EMAIL = _settings.get("contact.from", ADMINS[0][1])
DEFAULT_FROM_EMAIL = _settings.get("contact.from", ADMINS[0][1])
MANAGERS = _settings.get("contact.admins", ADMINS)

if _settings.get("mail.server.host", None):
    EMAIL_HOST = _settings.get("mail.server.host")
    EMAIL_USE_TLS = _settings.get("mail.server.tls", False)
    EMAIL_USE_SSL = _settings.get("mail.server.ssl", False)
    if _settings.get("mail.server.port", None):
        EMAIL_PORT = _settings.get("mail.server.port")
    if _settings.get("mail.server.user", None):
        EMAIL_HOST_USER = _settings.get("mail.server.user")
        EMAIL_HOST_PASSWORD = _settings.get("mail.server.password")

TEMPLATED_EMAIL_BACKEND = "templated_email.backends.vanilla_django"
TEMPLATED_EMAIL_AUTO_PLAIN = True

DYNAMIC_PREFERENCES = {
    "REGISTRY_MODULE": "preferences",
}

MAINTENANCE_MODE = _settings.get("maintenance.enabled", None)
MAINTENANCE_MODE_IGNORE_IP_ADDRESSES = _settings.get(
    "maintenance.ignore_ips", _settings.get("maintenance.internal_ips", [])
)
MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS = "aleksis.core.util.core_helpers.get_ip"
MAINTENANCE_MODE_IGNORE_SUPERUSER = True
MAINTENANCE_MODE_STATE_FILE_NAME = _settings.get(
    "maintenance.statefile", "maintenance_mode_state.txt"
)
MAINTENANCE_MODE_STATE_BACKEND = "maintenance_mode.backends.DefaultStorageBackend"

DBBACKUP_STORAGE = _settings.get("backup.storage", "django.core.files.storage.FileSystemStorage")
DBBACKUP_STORAGE_OPTIONS = {"location": _settings.get("backup.location", "/var/backups/aleksis")}
DBBACKUP_CLEANUP_KEEP = _settings.get("backup.database.keep", 10)
DBBACKUP_CLEANUP_KEEP_MEDIA = _settings.get("backup.media.keep", 10)
DBBACKUP_GPG_RECIPIENT = _settings.get("backup.gpg_recipient", None)
DBBACKUP_COMPRESS_DB = _settings.get("backup.database.compress", True)
DBBACKUP_ENCRYPT_DB = _settings.get("backup.database.encrypt", DBBACKUP_GPG_RECIPIENT is not None)
DBBACKUP_COMPRESS_MEDIA = _settings.get("backup.media.compress", True)
DBBACKUP_ENCRYPT_MEDIA = _settings.get("backup.media.encrypt", DBBACKUP_GPG_RECIPIENT is not None)
DBBACKUP_CLEANUP_DB = _settings.get("backup.database.clean", True)
DBBACKUP_CLEANUP_MEDIA = _settings.get("backup.media.clean", True)
DBBACKUP_CONNECTOR_MAPPING = {
    "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
}

if _settings.get("backup.storage.type", "").lower() == "s3":
    DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"

    DBBACKUP_STORAGE_OPTIONS = {
        key: value for (key, value) in _settings.get("backup.storage.s3").items()
    }

IMPERSONATE = {"REQUIRE_SUPERUSER": True, "ALLOW_SUPERUSER": True, "REDIRECT_FIELD_NAME": "next"}

DJANGO_TABLES2_TEMPLATE = "django_tables2/materialize.html"

ANONYMIZE_ENABLED = _settings.get("maintenance.anonymisable", True)

LOGIN_URL = "two_factor:login"

if _settings.get("2fa.call.enabled", False):
    if "two_factor.middleware.threadlocals.ThreadLocals" not in MIDDLEWARE:
        MIDDLEWARE.insert(
            MIDDLEWARE.index("django_otp.middleware.OTPMiddleware") + 1,
            "two_factor.middleware.threadlocals.ThreadLocals",
        )
    TWO_FACTOR_CALL_GATEWAY = "two_factor.gateways.twilio.gateway.Twilio"

if _settings.get("2fa.sms.enabled", False):
    if "two_factor.middleware.threadlocals.ThreadLocals" not in MIDDLEWARE:
        MIDDLEWARE.insert(
            MIDDLEWARE.index("django_otp.middleware.OTPMiddleware") + 1,
            "two_factor.middleware.threadlocals.ThreadLocals",
        )
    TWO_FACTOR_SMS_GATEWAY = "two_factor.gateways.twilio.gateway.Twilio"

if _settings.get("twilio.sid", None):
    TWILIO_ACCOUNT_SID = _settings.get("twilio.sid")
    TWILIO_AUTH_TOKEN = _settings.get("twilio.token")
    TWILIO_CALLER_ID = _settings.get("twilio.callerid")

TWO_FACTOR_WEBAUTHN_RP_NAME = _settings.get("2fa.webauthn.rp_name", "AlekSIS")

CELERY_BROKER_URL = _settings.get("celery.broker", REDIS_URL)
CELERY_RESULT_BACKEND = "django-db"
CELERY_CACHE_BACKEND = "django-cache"
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
CELERY_RESULT_EXTENDED = True

if _settings.get("celery.email", False):
    EMAIL_BACKEND = "djcelery_email.backends.CeleryEmailBackend"

if _settings.get("dev.uwsgi.celery", DEBUG):
    concurrency = _settings.get("celery.uwsgi.concurrency", 2)
    UWSGI.setdefault("attach-daemon", [])
    UWSGI["attach-daemon"].append(f"celery -A aleksis.core worker --concurrency={concurrency}")
    UWSGI["attach-daemon"].append("celery -A aleksis.core beat")
    UWSGI["attach-daemon"].append("aleksis-admin vite --no-install serve")

DEFAULT_FAVICON_PATHS = {
    "pwa_icon": os.path.join(STATIC_ROOT, "img/aleksis-icon-maskable.png"),
    "favicon": os.path.join(STATIC_ROOT, "img/aleksis-favicon.png"),
}
PWA_ICONS_CONFIG = {
    "android": [192, 512],
    "apple": [76, 114, 152, 180],
    "apple_splash": [192],
    "microsoft": [144],
}
FAVICON_PATH = os.path.join("public", "favicon")
FAVICON_CONFIG = {
    "shortcut icon": [16, 32, 48, 128, 192],
    "touch-icon": [196],
    "icon": [196],
}

SERVICE_WORKER_PATH = os.path.join(STATIC_ROOT, "sw.js")

SITE_ID = 1

CKEDITOR_CONFIGS = {
    "default": {
        "toolbar_Basic": [["Source", "-", "Bold", "Italic"]],
        "toolbar_Full": [
            {
                "name": "document",
                "items": ["Source", "-", "Save", "NewPage", "Preview", "Print", "-", "Templates"],
            },
            {
                "name": "clipboard",
                "items": [
                    "Cut",
                    "Copy",
                    "Paste",
                    "PasteText",
                    "PasteFromWord",
                    "-",
                    "Undo",
                    "Redo",
                ],
            },
            {"name": "editing", "items": ["Find", "Replace", "-", "SelectAll"]},
            {
                "name": "insert",
                "items": [
                    "Image",
                    "Table",
                    "HorizontalRule",
                    "Smiley",
                    "SpecialChar",
                    "PageBreak",
                    "Iframe",
                ],
            },
            "/",
            {
                "name": "basicstyles",
                "items": [
                    "Bold",
                    "Italic",
                    "Underline",
                    "Strike",
                    "Subscript",
                    "Superscript",
                    "-",
                    "RemoveFormat",
                ],
            },
            {
                "name": "paragraph",
                "items": [
                    "NumberedList",
                    "BulletedList",
                    "-",
                    "Outdent",
                    "Indent",
                    "-",
                    "Blockquote",
                    "CreateDiv",
                    "-",
                    "JustifyLeft",
                    "JustifyCenter",
                    "JustifyRight",
                    "JustifyBlock",
                    "-",
                    "BidiLtr",
                    "BidiRtl",
                    "Language",
                ],
            },
            {"name": "links", "items": ["Link", "Unlink", "Anchor"]},
            "/",
            {"name": "styles", "items": ["Styles", "Format", "Font", "FontSize"]},
            {"name": "colors", "items": ["TextColor", "BGColor"]},
            {"name": "tools", "items": ["Maximize", "ShowBlocks"]},
            {"name": "about", "items": ["About"]},
            {
                "name": "customtools",
                "items": [
                    "Preview",
                    "Maximize",
                ],
            },
        ],
        "toolbar": "Full",
        "tabSpaces": 4,
        "extraPlugins": ",".join(
            [
                "uploadimage",
                "div",
                "autolink",
                "autoembed",
                "embedsemantic",
                "autogrow",
                # 'devtools',
                "widget",
                "lineutils",
                "clipboard",
                "dialog",
                "dialogui",
                "elementspath",
            ]
        ),
    }
}

# Upload path for CKEditor. Relative to MEDIA_ROOT.
CKEDITOR_UPLOAD_PATH = "ckeditor_uploads/"

# Which HTML tags are allowed
BLEACH_ALLOWED_TAGS = ["p", "b", "i", "u", "em", "strong", "a", "div"]

# Which HTML attributes are allowed
BLEACH_ALLOWED_ATTRIBUTES = ["href", "title", "style"]

# Which CSS properties are allowed in 'style' attributes (assuming
# style is an allowed attribute)
BLEACH_ALLOWED_STYLES = ["font-family", "font-weight", "text-decoration", "font-variant"]

# Strip unknown tags if True, replace with HTML escaped characters if
# False
BLEACH_STRIP_TAGS = True

# Strip comments, or leave them in.
BLEACH_STRIP_COMMENTS = True

LOGGING = deepcopy(DEFAULT_LOGGING)
# Set root logging level as default
LOGGING["root"] = {
    "handlers": ["console"],
    "level": _settings.get("logging.level", "WARNING"),
}
# Configure global log Format
LOGGING["formatters"]["verbose"] = {
    "format": "{asctime} {levelname} {name}[{process}]: {message}",
    "style": "{",
}
# Add null handler for selective silencing
LOGGING["handlers"]["null"] = {"class": "logging.NullHandler"}
# Make console logging independent of DEBUG
LOGGING["handlers"]["console"]["filters"].remove("require_debug_true")
# Use root log level for console
del LOGGING["handlers"]["console"]["level"]
# Use verbose log format for console
LOGGING["handlers"]["console"]["formatter"] = "verbose"
# Disable exception mails if not desired
if not _settings.get("logging.mail_admins", True):
    LOGGING["loggers"]["django"]["handlers"].remove("mail_admins")
# Disable mails on disaalowed host by default
if not _settings.get("logging.disallowed_host", False):
    LOGGING["loggers"]["django.security.DisallowedHost"] = {
        "handlers": ["null"],
        "propagate": False,
    }
# Configure logging explicitly for Celery
LOGGING["loggers"]["celery"] = {
    "handlers": ["console"],
    "level": _settings.get("logging.level", "WARNING"),
    "propagate": False,
}
# Set Django log levels
LOGGING["loggers"]["django"]["level"] = _settings.get("logging.level", "WARNING")
LOGGING["loggers"]["django.server"]["level"] = _settings.get("logging.level", "WARNING")

# Rules and permissions

GUARDIAN_RAISE_403 = True
ANONYMOUS_USER_NAME = None

SILENCED_SYSTEM_CHECKS.append("guardian.W001")

# Append authentication backends
AUTHENTICATION_BACKENDS.append("rules.permissions.ObjectPermissionBackend")

HAYSTACK_CONNECTIONS = {
    "default": {
        "ENGINE": "haystack_redis.RedisEngine",
        "PATH": REDIS_URL,
    },
}

HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor"
CELERY_HAYSTACK_IGNORE_RESULT = True

HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10

DJANGO_EASY_AUDIT_WATCH_REQUEST_EVENTS = False

HEALTH_CHECK = {
    "DISK_USAGE_MAX": _settings.get("health.disk_usage_max_percent", 90),
    "MEMORY_MIN": _settings.get("health.memory_min_mb", 500),
}

DBBACKUP_CHECK_SECONDS = _settings.get("backup.database.check_seconds", 7200)
MEDIABACKUP_CHECK_SECONDS = _settings.get("backup.media.check_seconds", 7200)

PROMETHEUS_EXPORT_MIGRATIONS = False
PROMETHEUS_METRICS_EXPORT_PORT = _settings.get("prometheus.metrics.port", None)
PROMETHEUS_METRICS_EXPORT_ADDRESS = _settings.get("prometheus.metrucs.address", None)

SECURE_PROXY_SSL_HEADER = ("REQUEST_SCHEME", "https")

FILE_UPLOAD_HANDLERS = [
    "django.core.files.uploadhandler.MemoryFileUploadHandler",
    "django.core.files.uploadhandler.TemporaryFileUploadHandler",
]

if _settings.get("storage.type", "").lower() == "s3":
    INSTALLED_APPS.append("storages")

    DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
    FILE_UPLOAD_HANDLERS.remove("django.core.files.uploadhandler.MemoryFileUploadHandler")

    if _settings.get("storage.s3.static.enabled", False):
        STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"
        AWS_STORAGE_BUCKET_NAME_STATIC = _settings.get("storage.s3.static.bucket_name", "")
        AWS_S3_MAX_AGE_SECONDS_CACHED_STATIC = _settings.get(
            "storage.s3.static.max_age_seconds", 24 * 60 * 60
        )

    AWS_REGION = _settings.get("storage.s3.region_name", "")
    AWS_ACCESS_KEY_ID = _settings.get("storage.s3.access_key", "")
    AWS_SECRET_ACCESS_KEY = _settings.get("storage.s3.secret_key", "")
    AWS_SESSION_TOKEN = _settings.get("storage.s3.session_token", "")
    AWS_STORAGE_BUCKET_NAME = _settings.get("storage.s3.bucket_name", "")
    AWS_LOCATION = _settings.get("storage.s3.location", "")
    AWS_S3_ADDRESSING_STYLE = _settings.get("storage.s3.addressing_style", "auto")
    AWS_S3_ENDPOINT_URL = _settings.get("storage.s3.endpoint_url", "")
    AWS_S3_KEY_PREFIX = _settings.get("storage.s3.key_prefix", "")
    AWS_S3_BUCKET_AUTH = _settings.get("storage.s3.bucket_auth", True)
    AWS_S3_MAX_AGE_SECONDS = _settings.get("storage.s3.max_age_seconds", 24 * 60 * 60)
    AWS_S3_PUBLIC_URL = _settings.get("storage.s3.public_url", "")
    AWS_S3_REDUCED_REDUNDANCY = _settings.get("storage.s3.reduced_redundancy", False)
    AWS_S3_CONTENT_DISPOSITION = _settings.get("storage.s3.content_disposition", "")
    AWS_S3_CONTENT_LANGUAGE = _settings.get("storage.s3.content_language", "")
    AWS_S3_METADATA = _settings.get("storage.s3.metadata", {})
    AWS_S3_ENCRYPT_KEY = _settings.get("storage.s3.encrypt_key", False)
    AWS_S3_KMS_ENCRYPTION_KEY_ID = _settings.get("storage.s3.kms_encryption_key_id", "")
    AWS_S3_GZIP = _settings.get("storage.s3.gzip", True)
    AWS_S3_SIGNATURE_VERSION = _settings.get("storage.s3.signature_version", None)
    AWS_S3_FILE_OVERWRITE = _settings.get("storage.s3.file_overwrite", False)
    AWS_S3_VERIFY = _settings.get("storage.s3.verify", True)
    AWS_S3_USE_SSL = _settings.get("storage.s3.use_ssl", True)
else:
    DEFAULT_FILE_STORAGE = "titofisto.TitofistoStorage"
    TITOFISTO_TIMEOUT = 10 * 60

TITOFISTO_ENABLE_UPLOAD = True
TITOFISTO_UPLOAD_NAMESPACE = "__titofisto__/upload/"

SASS_PROCESSOR_STORAGE = DEFAULT_FILE_STORAGE

SENTRY_ENABLED = _settings.get("health.sentry.enabled", False)
if SENTRY_ENABLED:
    import sentry_sdk
    from sentry_sdk.integrations.celery import CeleryIntegration
    from sentry_sdk.integrations.django import DjangoIntegration
    from sentry_sdk.integrations.redis import RedisIntegration

    from aleksis.core import __version__

    SENTRY_SETTINGS = {
        "dsn": _settings.get("health.sentry.dsn"),
        "environment": _settings.get("health.sentry.environment"),
        "traces_sample_rate": _settings.get("health.sentry.traces_sample_rate", 1.0),
        "send_default_pii": _settings.get("health.sentry.send_default_pii", False),
        "release": f"aleksis-core@{__version__}",
        "in_app_include": "aleksis",
    }
    sentry_sdk.init(
        integrations=[
            DjangoIntegration(transaction_style="function_name"),
            RedisIntegration(),
            CeleryIntegration(),
        ],
        **SENTRY_SETTINGS,
    )

SHELL_PLUS_MODEL_IMPORTS_RESOLVER = "django_extensions.collision_resolvers.AppLabelPrefixCR"
SHELL_PLUS_APP_PREFIXES = {
    "auth": "auth",
}
SHELL_PLUS_DONT_LOAD = []
merge_app_settings("SHELL_PLUS_APP_PREFIXES", SHELL_PLUS_APP_PREFIXES)
merge_app_settings("SHELL_PLUS_DONT_LOAD", SHELL_PLUS_DONT_LOAD)

X_FRAME_OPTIONS = "SAMEORIGIN"

# Add django-cleanup after all apps to ensure that it gets all signals as last app
INSTALLED_APPS.append("django_cleanup.apps.CleanupConfig")

locals().update(get_app_settings_overrides())