minor: валидатор форм, парсера и моделей (26) в save модели TbOffer автоматически создаем хаотичные коды для скрытия id (чтобы злоумышленники не проворачивали махинации с заказами и покупками. Код товара выглядит хаотично и не поддается расшифровки без секретного ключа).
This commit is contained in:
@@ -8,6 +8,19 @@ DJANGO_ADMINS=YouName:you@email.com
|
|||||||
# Укажите здесь URL, по которому вы заходите на сайт (с протоколом и портом gunicorn)
|
# Укажите здесь URL, по которому вы заходите на сайт (с протоколом и портом gunicorn)
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,http://testserver,https://lpon.ru
|
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,http://testserver,https://lpon.ru
|
||||||
|
|
||||||
|
# ====== HASHIDS настройка для s_offer_skip32 ======
|
||||||
|
# Используется для кодирования ID коммерческих предложений в компактные, хаотичные коды (не криптографический хеш, а encoder).
|
||||||
|
# - OFFER_HASHIDS_SALT: секретная соль для кодирования (ВАЖНО! Менять для production!)
|
||||||
|
# Только с солью можно декодировать, без нее невозможно восстановить ID
|
||||||
|
# Генерируйте криптографически стойкую случайную строку: python -c "import secrets; print(secrets.token_hex(16))"
|
||||||
|
# Содержит только буквы и цифры (безопасно для URL и QR-кода)
|
||||||
|
# - OFFER_HASHIDS_MIN_LENGTH: минимальная длина кода
|
||||||
|
# 6 символов = оптимально для каталогов до 100k коммерческих предложений
|
||||||
|
# 8 символов = для каталогов 100k-10M коммерческих предложений
|
||||||
|
# Рекомендация: оставить 6 для начальной фазы, поменять на 8 при росте
|
||||||
|
OFFER_HASHIDS_SALT=your-super-secret-salt-for-production-change-me-now
|
||||||
|
OFFER_HASHIDS_MIN_LENGTH=6
|
||||||
|
|
||||||
# URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти)
|
# URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти)
|
||||||
DJANGO_ADMIN_URL=admin/
|
DJANGO_ADMIN_URL=admin/
|
||||||
|
|
||||||
|
|||||||
@@ -240,8 +240,10 @@
|
|||||||
# - M2M использует числовые FK (INT) вместо строк
|
# - M2M использует числовые FK (INT) вместо строк
|
||||||
# - Slug'и как UNIQUE indexed fields (не primary_key) для экономии места
|
# - Slug'и как UNIQUE indexed fields (не primary_key) для экономии места
|
||||||
|
|
||||||
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from hashids import Hashids
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
@@ -257,6 +259,8 @@ from lpon_site.settings import (
|
|||||||
KEY_IMAGE_NOTE, # Ключ, для заметок о картинке (например, "обложка", "задник", "вкладка" и т.д.)
|
KEY_IMAGE_NOTE, # Ключ, для заметок о картинке (например, "обложка", "задник", "вкладка" и т.д.)
|
||||||
KEY_OFFER_NOTE, # Ключ, для заметок о коммерческом предложении (например, "замят угол конверта" и т.д.)
|
KEY_OFFER_NOTE, # Ключ, для заметок о коммерческом предложении (например, "замят угол конверта" и т.д.)
|
||||||
KEY_OFFER_ALL_MEDIA, # Ключ, для хранения информации о том, какие носители входят в оффер (например, CD+2LP)
|
KEY_OFFER_ALL_MEDIA, # Ключ, для хранения информации о том, какие носители входят в оффер (например, CD+2LP)
|
||||||
|
OFFER_HASHIDS_SALT, # Соль для криптографического кодирования ID оферов
|
||||||
|
OFFER_HASHIDS_MIN_LENGTH, # Минимальная длина кода
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1341,6 +1345,55 @@ class TbOffer(models.Model):
|
|||||||
i_offer_favorites=F('i_offer_favorites') + 1
|
i_offer_favorites=F('i_offer_favorites') + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Переопределенный метод save для автоматического формирования s_offer_skip32.
|
||||||
|
|
||||||
|
Уникальный код товара формируется в двух случаях:
|
||||||
|
1. Для новых оферов (при первом сохранении)
|
||||||
|
2. Для старых оферов БЕЗ s_offer_skip32 (миграция существующих данных)
|
||||||
|
|
||||||
|
Использует криптографическое хеширование (hashids):
|
||||||
|
- Невозможно восстановить исходный ID из кода
|
||||||
|
- Даже администратор, зная алгоритм, не может нарушить безопасность
|
||||||
|
- Коды выглядят случайно и не идут подряд
|
||||||
|
- Компактные (6-12 символов, настраивается в settings)
|
||||||
|
|
||||||
|
Примеры: ID 1 → "q1am12", ID 42 → "QBErd8", ID 1000 → "kd8GgB", ID 9999 → "1GYeRd"
|
||||||
|
|
||||||
|
ЛОГИКА:
|
||||||
|
- Если это новый оффер (self.pk == None):
|
||||||
|
1. super().save() → Django создает запись в БД и присваивает self.pk
|
||||||
|
2. Кодируем self.pk в хеш и сохраняем еще раз
|
||||||
|
|
||||||
|
- Если это старый оффер БЕЗ s_offer_skip32 (миграция):
|
||||||
|
1. Кодируем существующий self.pk в хеш
|
||||||
|
2. Сохраняем с обновленным s_offer_skip32
|
||||||
|
|
||||||
|
- Если это старый оффер С s_offer_skip32:
|
||||||
|
1. Сохраняем как обычно, не трогаем s_offer_skip32
|
||||||
|
"""
|
||||||
|
# Проверяем нужно ли генерировать s_offer_skip32:
|
||||||
|
# - ИЛИ это новый объект (self.pk == None)
|
||||||
|
# - ИЛИ это старый объект без s_offer_skip32 (миграция)
|
||||||
|
if not self.pk or not self.s_offer_skip32:
|
||||||
|
# Если это новый оффер, сначала сохраняем, чтобы получить ID
|
||||||
|
if not self.pk:
|
||||||
|
# Сохраняем БЕЗ s_offer_skip32 чтобы Django создал запись и присвоил pk
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
# После save() Django автоматически заполнит self.pk
|
||||||
|
|
||||||
|
# Кодируем pk в компактный, необратимый код
|
||||||
|
# Пример: pk=42 → "QBErd8"
|
||||||
|
self.s_offer_skip32 = Hashids(salt=OFFER_HASHIDS_SALT, min_length=OFFER_HASHIDS_MIN_LENGTH).encode(self.pk)
|
||||||
|
|
||||||
|
# Сохраняем только поле s_offer_skip32 (не перезаписываем остальное)
|
||||||
|
super().save(update_fields=['s_offer_skip32'])
|
||||||
|
else:
|
||||||
|
# Оффер существует И уже имеет s_offer_skip32: сохраняем как обычно
|
||||||
|
# Не трогаем s_offer_skip32, он был сформирован при создании
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Оффер (предложение)'
|
verbose_name = 'Оффер (предложение)'
|
||||||
verbose_name_plural = 'Офферы (предложения)'
|
verbose_name_plural = 'Офферы (предложения)'
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ ALLOWED_HOSTS = env.list(
|
|||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=['127.0.0.1', 'localhost', 'testserver'])
|
CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=['127.0.0.1', 'localhost', 'testserver'])
|
||||||
|
|
||||||
|
# ====== HASHIDS настройка для s_offer_skip32 ======
|
||||||
|
# Криптографическое кодирование ID оферов в компактные, необратимые коды.
|
||||||
|
# Благодаря hashids невозможно восстановить исходный ID из кода,
|
||||||
|
# даже зная алгоритм (в отличие от простого XOR).
|
||||||
|
OFFER_HASHIDS_SALT = env('OFFER_HASHIDS_SALT', default='your-secret-salt-change-me')
|
||||||
|
OFFER_HASHIDS_MIN_LENGTH = env.int('OFFER_HASHIDS_MIN_LENGTH', default=6)
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
# Настройки сообщений об ошибках когда все упало и т.п.
|
# Настройки сообщений об ошибках когда все упало и т.п.
|
||||||
ADMINS = tuple(
|
ADMINS = tuple(
|
||||||
|
|||||||
Generated
+15
-1
@@ -314,6 +314,20 @@ beautifulsoup4 = ">=4.10.0"
|
|||||||
lxml = ">=4.9.0"
|
lxml = ">=4.9.0"
|
||||||
regex = ">=2022.1.18"
|
regex = ">=2022.1.18"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashids"
|
||||||
|
version = "1.3.1"
|
||||||
|
description = "Implements the hashids algorithm in python. For more information, visit http://hashids.org/"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "hashids-1.3.1-py2.py3-none-any.whl", hash = "sha256:8bddd1acba501bfc9306e7e5a99a1667f4f2cacdc20cbd70bcc5ddfa5147c94c"},
|
||||||
|
{file = "hashids-1.3.1.tar.gz", hash = "sha256:6c3dc775e65efc2ce2c157a65acb776d634cb814598f406469abef00ae3f635c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest (>=2.1.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lxml"
|
name = "lxml"
|
||||||
version = "6.1.1"
|
version = "6.1.1"
|
||||||
@@ -903,4 +917,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "69bad093bc7511b8da08bb4673d5883f7cbeb9b45123b6a17350dd885e2a3a33"
|
content-hash = "df0a39ef96e82584ad911b561106eeb7559db3c6e78fb0c3a8b53a80738620b9"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ django-filer = "^3.4.4"
|
|||||||
pillow-heif = "^1.3.0"
|
pillow-heif = "^1.3.0"
|
||||||
pytils = "^0.4.4"
|
pytils = "^0.4.4"
|
||||||
etpgrf = "^0.1.6.post1"
|
etpgrf = "^0.1.6.post1"
|
||||||
|
hashids = "^1.3.1"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
django-extensions = "^3.2"
|
django-extensions = "^3.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user