diff --git a/config/settings.py b/config/settings.py index 482dd83..0935d88 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,12 +1,26 @@ """Development settings for the teacher consultation website.""" +import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent -SECRET_KEY = "django-insecure-change-this-key-before-production" -DEBUG = True -ALLOWED_HOSTS = ["127.0.0.1", "localhost"] +# --- Секреты только из окружения: без env приложение не стартует --- +SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") +if not SECRET_KEY: + raise RuntimeError( + "DJANGO_SECRET_KEY не задан. Установите переменную окружения " + "(например: python -c \"from django.core.management.utils " + "import get_random_secret_key; print(get_random_secret_key())\")." + ) + +DEBUG = os.environ.get("DJANGO_DEBUG") == "1" + +ALLOWED_HOSTS = [ + host.strip() + for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",") + if host.strip() +] INSTALLED_APPS = [ "django.contrib.admin", @@ -15,6 +29,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "axes", "consultations", ] @@ -24,10 +39,16 @@ MIDDLEWARE = [ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "axes.middleware.AxesMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] +# --- django-axes: защита от брутфорса логина --- +AXES_FAILURE_LIMIT = 5 +AXES_COOLOFF_TIME = 1 # часы +AXES_RESET_ON_SUCCESS = True + ROOT_URLCONF = "config.urls" TEMPLATES = [ @@ -48,6 +69,12 @@ TEMPLATES = [ WSGI_APPLICATION = "config.wsgi.application" ASGI_APPLICATION = "config.asgi.application" +# django-axes: бэкенд аутентификации для защиты от брутфорса +AUTHENTICATION_BACKENDS = [ + "axes.backends.AxesStandaloneBackend", + "django.contrib.auth.backends.ModelBackend", +] + DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", @@ -55,7 +82,21 @@ DATABASES = { } } -AUTH_PASSWORD_VALIDATORS = [] +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + "OPTIONS": {"min_length": 8}, + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] LANGUAGE_CODE = "ru-ru" TIME_ZONE = "Europe/Astrakhan" diff --git a/consultations/forms.py b/consultations/forms.py index abfbb19..ac2686b 100644 --- a/consultations/forms.py +++ b/consultations/forms.py @@ -73,3 +73,9 @@ class ConsultationRequestForm(forms.ModelForm): if slot and requested_format not in (ConsultationRequest.Format.ANY, slot.format): self.add_error("format", f"Для выбранного времени доступен формат: {slot.get_format_display()}.") return cleaned_data + + def clean_consent_given(self): + consent = self.cleaned_data.get("consent_given") + if not consent: + raise forms.ValidationError("Необходимо согласие на обработку персональных данных.") + return consent diff --git a/requirements.txt b/requirements.txt index ca6c5c5..4322ec8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Django>=5.0,<6.0 +django-axes>=8.3.1