diff --git a/.env.example b/.env.example index 210920f..82b810a 100644 --- a/.env.example +++ b/.env.example @@ -5,8 +5,12 @@ # Сгенерировать: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" DJANGO_SECRET_KEY= -# Включить режим отладки (ТОЛЬКО для разработки!): 1 = да, пусто/0 = нет -DJANGO_DEBUG=1 +# Режим отладки: для продакшена строго 0 (пусто). 1 — только локальная разработка! +DJANGO_DEBUG=0 -# Разрешённые хосты через запятую (для продакшена: ваш домен) -DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost +# Разрешённые хосты через запятую (для продакшена: ваш домен, например teacher.labnethub.ru) +DJANGO_ALLOWED_HOSTS=teacher.labnethub.ru,127.0.0.1,localhost + +# Secure-куки и HSTS (сайт за HTTPS-прокси): держать включёнными (=1) +DJANGO_COOKIE_SECURE=1 +DJANGO_HSTS_SECONDS=31536000 diff --git a/config/settings.py b/config/settings.py index 3aa0e0e..7195068 100644 --- a/config/settings.py +++ b/config/settings.py @@ -50,10 +50,37 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] +# --- HTTPS / безопасность кук (сайт работает только через HTTPS-прокси) --- +# nginx должен передавать заголовок X-Forwarded-Proto: https +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SESSION_COOKIE_SECURE = os.environ.get("DJANGO_COOKIE_SECURE", "1") == "1" +CSRF_COOKIE_SECURE = os.environ.get("DJANGO_COOKIE_SECURE", "1") == "1" +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = "Lax" +SECURE_CONTENT_TYPE_NOSNIFF = True +SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin" +X_FRAME_OPTIONS = "DENY" +SECURE_HSTS_SECONDS = int(os.environ.get("DJANGO_HSTS_SECONDS", "31536000")) +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +# Редирект на HTTPS не включаем — его делает nginx (иначе цикл при неправильном X-Forwarded-Proto) +# SECURE_SSL_REDIRECT = True + # --- django-axes: защита от брутфорса логина --- AXES_FAILURE_LIMIT = 5 AXES_COOLOFF_TIME = 1 # часы AXES_RESET_ON_SUCCESS = True +AXES_LOCKOUT_PARAMETERS = ["ip_address"] + +# --- антиспам: лимит заявок с одного IP --- +# Кэш in-memory (locmem) подходит для одного воркера; для нескольких воркеров +# нужно memcached/redis, иначе лимит считается на каждый процесс отдельно. +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "teacher-site-cache", + } +} ROOT_URLCONF = "config.urls" diff --git a/consultations/views.py b/consultations/views.py index fbd73df..3b77ecf 100644 --- a/consultations/views.py +++ b/consultations/views.py @@ -1,14 +1,48 @@ from django.contrib import messages +from django.core.cache import cache from django.db import IntegrityError, transaction from django.shortcuts import redirect, render from django.urls import reverse +from django.utils import timezone from .forms import ConsultationRequestForm from .models import ConsultationSlot +# Антиспам: не более 3 заявок с одного IP за 10 минут +SUBMIT_LIMIT = 3 +SUBMIT_WINDOW_SECONDS = 600 + + +def _client_ip(request): + # X-Forwarded-For добавляет nginx; берём первый адрес (реальный клиент) + xff = request.META.get("HTTP_X_FORWARDED_FOR") + if xff: + return xff.split(",")[0].strip() + return request.META.get("REMOTE_ADDR", "unknown") + + +def _submit_allowed(request): + key = f"submit:{_client_ip(request)}" + now = timezone.now().timestamp() + stamps = cache.get(key, []) + stamps = [s for s in stamps if now - s < SUBMIT_WINDOW_SECONDS] + if len(stamps) >= SUBMIT_LIMIT: + cache.set(key, stamps, SUBMIT_WINDOW_SECONDS) + return False + stamps.append(now) + cache.set(key, stamps, SUBMIT_WINDOW_SECONDS) + return True + + def home(request): if request.method == "POST": + if not _submit_allowed(request): + messages.error( + request, + "Слишком много заявок за короткое время. Попробуйте через 10 минут.", + ) + return redirect(f"{reverse('consultations:home')}#booking") form = ConsultationRequestForm(request.POST) if form.is_valid(): try: diff --git a/main.py b/main.py deleted file mode 100644 index 5596b44..0000000 --- a/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# This is a sample Python script. - -# Press Shift+F10 to execute it or replace it with your code. -# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. - - -def print_hi(name): - # Use a breakpoint in the code line below to debug your script. - print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint. - - -# Press the green button in the gutter to run the script. -if __name__ == '__main__': - print_hi('PyCharm') - -# See PyCharm help at https://www.jetbrains.com/help/pycharm/