import logging from django.db import models from core.utils.crypto import encrypt_secret, decrypt_secret from core.utils.validators import ( normalize_ukrainian_phone, phone_input_attrs, validate_ukrainian_phone, ) logger = logging.getLogger(__name__) class UkrainianPhoneField(models.CharField): """Украинский телефон: в БД всегда +380XXXXXXXXX, в форме — маска.""" default_validators = [validate_ukrainian_phone] def __init__(self, *args, **kwargs): kwargs.setdefault('max_length', 20) super().__init__(*args, **kwargs) def to_python(self, value): value = super().to_python(value) if value in (None, ''): return value return normalize_ukrainian_phone(value) def get_prep_value(self, value): value = super().get_prep_value(value) if value in (None, ''): return value return normalize_ukrainian_phone(value) def formfield(self, **kwargs): from django import forms kwargs.setdefault('widget', forms.TextInput(attrs=phone_input_attrs())) return super().formfield(**kwargs) class EncryptedCharField(models.CharField): """CharField, который хранит значение в БД в зашифрованном виде (Fernet). Для приложения поле ведёт себя как обычная строка: чтение отдаёт открытый текст, запись сохраняет шифртекст. Шифрование выполняется на границе с БД, поэтому читатели (`cfg.password`, `cfg.token`) и админка изменений не требуют. Шифртекст длиннее исходного текста (~1.4×) плюс постоянный оверхед Fernet, поэтому колонку нужно объявлять с запасом по `max_length`. """ def get_db_prep_save(self, value, connection): value = super().get_db_prep_save(value, connection) if value in (None, ''): return value return encrypt_secret(value) def from_db_value(self, value, expression, connection): if value in (None, ''): return value try: return decrypt_secret(value) except Exception: # Значение записано до включения шифрования — вернуть как есть. logger.warning( 'EncryptedCharField: decrypt failed, returning raw DB value ' '(check FIELD_ENCRYPTION_KEY rotation)' ) return value