Files
octopus/myproject/integrations/fields.py
Andrey Smakotin 37394121e1 feat(integrations): архитектура включения/выключения интеграций
- Удалена лишняя модель IntegrationConfig из system_settings
- Singleton-паттерн: одна запись на интеграцию с is_active тумблером
- Добавлено шифрование токенов (EncryptedCharField с Fernet AES-128)
- UI: тумблеры слева, форма настроек справа
- API endpoints: toggle, settings, form_data
- Модель Recommerce: store_url + api_token (x-auth-token)
- Модель WooCommerce: store_url + consumer_key/secret

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 00:29:04 +03:00

117 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Кастомные поля Django с шифрованием для безопасного хранения credentials.
Использует Fernet (AES-128-CBC) из библиотеки cryptography.
"""
from django.db import models
from django.conf import settings
from cryptography.fernet import Fernet, InvalidToken
class EncryptedCharField(models.CharField):
"""
CharField с прозрачным шифрованием/дешифрованием.
Данные шифруются при сохранении в БД и дешифруются при чтении.
В БД хранится зашифрованная строка (base64).
Требует ENCRYPTION_KEY в settings.py:
from cryptography.fernet import Fernet
ENCRYPTION_KEY = Fernet.generate_key() # сгенерировать один раз!
Пример использования:
api_token = EncryptedCharField(max_length=500, blank=True)
"""
description = "Encrypted CharField using Fernet"
def __init__(self, *args, **kwargs):
# Зашифрованные данные длиннее исходных, увеличиваем max_length
if 'max_length' in kwargs:
# Fernet добавляет ~100 байт overhead
kwargs['max_length'] = max(kwargs['max_length'] * 2, 500)
super().__init__(*args, **kwargs)
def _get_fernet(self):
"""Получить инстанс Fernet с ключом из settings"""
key = getattr(settings, 'ENCRYPTION_KEY', None)
if not key:
raise ValueError(
"ENCRYPTION_KEY не найден в settings. "
"Сгенерируйте ключ: from cryptography.fernet import Fernet; Fernet.generate_key()"
)
# Ключ может быть строкой или bytes
if isinstance(key, str):
key = key.encode()
return Fernet(key)
def get_prep_value(self, value):
"""Шифрование перед сохранением в БД"""
value = super().get_prep_value(value)
if value is None or value == '':
return value
try:
f = self._get_fernet()
encrypted = f.encrypt(value.encode('utf-8'))
return encrypted.decode('utf-8')
except Exception as e:
raise ValueError(f"Ошибка шифрования: {e}")
def from_db_value(self, value, expression, connection):
"""Дешифрование при чтении из БД"""
if value is None or value == '':
return value
try:
f = self._get_fernet()
decrypted = f.decrypt(value.encode('utf-8'))
return decrypted.decode('utf-8')
except InvalidToken:
# Данные не зашифрованы или ключ изменился
# Возвращаем как есть (для миграции старых данных)
return value
except Exception:
return value
def to_python(self, value):
"""Преобразование в Python-объект (не дешифруем, т.к. это для форм)"""
return super().to_python(value)
class EncryptedTextField(models.TextField):
"""
TextField с шифрованием для больших данных (например JSON credentials).
"""
description = "Encrypted TextField using Fernet"
def _get_fernet(self):
key = getattr(settings, 'ENCRYPTION_KEY', None)
if not key:
raise ValueError("ENCRYPTION_KEY не найден в settings.")
if isinstance(key, str):
key = key.encode()
return Fernet(key)
def get_prep_value(self, value):
value = super().get_prep_value(value)
if value is None or value == '':
return value
try:
f = self._get_fernet()
encrypted = f.encrypt(value.encode('utf-8'))
return encrypted.decode('utf-8')
except Exception as e:
raise ValueError(f"Ошибка шифрования: {e}")
def from_db_value(self, value, expression, connection):
if value is None or value == '':
return value
try:
f = self._get_fernet()
decrypted = f.decrypt(value.encode('utf-8'))
return decrypted.decode('utf-8')
except InvalidToken:
return value
except Exception:
return value