5 Commits
Author SHA1 Message Date
prog1764 22102dbb7a security: закрыть уязвимости аудита
- 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 включены
2026-08-06 16:14:09 +04:00
prog1764 a01a0da25b fix(docker): whitenoise для отдачи статики в production (teacher.labnethub.ru отдавал 404 на /static/) 2026-08-05 13:13:22 +04:00
prog1764 357099accd docker: подготовка к запуску в контейнере (Django)
- Dockerfile: python:3.12-slim, migrate + collectstatic + gunicorn (0.0.0.0:8000)
- docker-compose.yml: порт 8000, env из .env, volume для SQLite
- .dockerignore: .env, db.sqlite3, staticfiles, кэш
- requirements.txt: + gunicorn
- settings.py: STATIC_ROOT для collectstatic
- проверено: manage.py check без ошибок
2026-08-04 20:23:51 +04:00
prog1764 e81f13cb7e docs+chore: .env.example, загрузка .env, README по настройке окружения (PRJ-5)
- settings.py: load_dotenv из .env (python-dotenv)
- requirements.txt: + python-dotenv
- .env.example: DJANGO_SECRET_KEY / DJANGO_DEBUG / DJANGO_ALLOWED_HOSTS с инструкциями
- README: секция настройки окружения, миграции (в т.ч. таблицы django-axes)
- проверено: manage.py check без ошибок, migrate создал таблицы axes
2026-08-04 20:07:57 +04:00
prog1764 bab18e8129 fix: SECRET_KEY/DEBUG из env, валидаторы паролей, django-axes, обязательное consent (PRJ-5)
- SECRET_KEY: только из env (DJANGO_SECRET_KEY), без дефолта приложение не стартует
- DEBUG: только DJANGO_DEBUG=1; ALLOWED_HOSTS из env
- AUTH_PASSWORD_VALIDATORS: стандартные 4 валидатора (мин. длина 8)
- django-axes: подключён (бэкенд, middleware, лимит 5 попыток, кулдаун 1ч)
- consent_given: обязательное согласие на обработку ПД в форме
2026-08-04 19:51:16 +04:00
10 changed files with 211 additions and 24 deletions
+13
View File
@@ -0,0 +1,13 @@
.git
.gitignore
.idea
__pycache__
*.pyc
*.pyo
.env
.venv
venv
db.sqlite3
staticfiles
logs
*.log
+16
View File
@@ -0,0 +1,16 @@
# Скопируйте этот файл в .env и заполните реальными значениями.
# .env никогда не должен попадать в git (уже в .gitignore).
# Обязательно. Секретный ключ Django.
# Сгенерировать: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
DJANGO_SECRET_KEY=
# Режим отладки: для продакшена строго 0 (пусто). 1 — только локальная разработка!
DJANGO_DEBUG=0
# Разрешённые хосты через запятую (для продакшена: ваш домен, например 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
+17
View File
@@ -0,0 +1,17 @@
# Python 3.12 slim
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Сначала зависимости — чтобы кэшировать слой
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Код приложения
COPY . .
# Миграции + статика при старте контейнера
CMD ["sh", "-c", "python manage.py migrate --noinput && python manage.py collectstatic --noinput && gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 2"]
+21 -3
View File
@@ -67,16 +67,34 @@
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
```
### 2. Подготовка базы
### 2. Настройка окружения
Скопируйте `.env.example` в `.env` и заполните:
```powershell
Copy-Item .env.example .env
```
Обязательные переменные (без них приложение не стартует):
| Переменная | Описание |
| --- | --- |
| `DJANGO_SECRET_KEY` | Секретный ключ Django. Сгенерировать: `python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"` |
| `DJANGO_DEBUG` | `1` для разработки, пусто/`0` для продакшена |
| `DJANGO_ALLOWED_HOSTS` | Разрешённые хосты через запятую (в проде — домен) |
`.env` в git не попадает (уже в `.gitignore`).
### 3. Подготовка базы
```powershell
.\.venv\Scripts\python.exe manage.py migrate
.\.venv\Scripts\python.exe manage.py createsuperuser
```
Команда `createsuperuser` попросит придумать логин и пароль для кабинета преподавателя.
`migrate` создаст таблицы проекта и django-axes (защита от брутфорса).
### 3. Запуск
### 4. Запуск
```powershell
.\.venv\Scripts\python.exe manage.py runserver
+86 -4
View File
@@ -1,12 +1,31 @@
"""Development settings for the teacher consultation website."""
import os
from pathlib import Path
from dotenv import load_dotenv
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 (если есть) — секреты не храним в коде.
load_dotenv(BASE_DIR / ".env")
# --- Секреты только из окружения: без 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,19 +34,54 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"axes",
"consultations",
]
MIDDLEWARE = [
"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",
"axes.middleware.AxesMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"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"
TEMPLATES = [
@@ -48,6 +102,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 +115,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"
@@ -63,6 +137,14 @@ USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+6
View File
@@ -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
+34
View File
@@ -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:
+14
View File
@@ -0,0 +1,14 @@
# Секреты — только из .env (см. .env.example)
services:
web:
build: .
ports:
- "8000:8000"
env_file:
- .env
volumes:
- sqlite_data:/app/db.sqlite3
restart: unless-stopped
volumes:
sqlite_data:
-16
View File
@@ -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/
+4 -1
View File
@@ -1,2 +1,5 @@
Django>=5.0,<6.0
django-axes>=8.3.1
python-dotenv>=1.0
gunicorn>=22.0
whitenoise>=6.6