- Secure-куки (SESSION/CSRF_COOKIE_SECURE), HSTS, nosniff, Referrer-Policy, X-Frame-Options - SECURE_PROXY_SSL_HEADER для корректной работы за HTTPS-прокси - антиспам: не более 3 заявок с одного IP за 10 минут (кэш) - удалён мусорный main.py (заглушка PyCharm) - .env.example: DJANGO_DEBUG=0, ALLOWED_HOSTS под прод, COOKIE_SECURE/HSTS включены
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
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:
|
|
with transaction.atomic():
|
|
form.save()
|
|
except IntegrityError:
|
|
form.add_error("slot", "Это время только что занял другой ученик. Выберите другой слот.")
|
|
else:
|
|
messages.success(
|
|
request,
|
|
"Заявка принята. Игорь Олегович свяжется с вами для подтверждения консультации.",
|
|
)
|
|
return redirect(f"{reverse('consultations:home')}#booking")
|
|
else:
|
|
form = ConsultationRequestForm()
|
|
|
|
return render(
|
|
request,
|
|
"consultations/home.html",
|
|
{
|
|
"form": form,
|
|
"available_slot_count": ConsultationSlot.objects.available().count(),
|
|
},
|
|
)
|
|
|
|
|
|
def privacy_policy(request):
|
|
return render(request, "consultations/privacy.html")
|