This commit is contained in:
2025-07-26 04:41:47 +03:00
parent 019e4f90c7
commit f5cf4c32da
17 changed files with 2386 additions and 931 deletions

106
models.py
View File

@@ -8,34 +8,36 @@ import logging
logger = logging.getLogger(__name__)
db = SQLAlchemy()
# 1. Инициализируем расширение без привязки к app
from extensions import db
# Generate a key for encryption. STORE THIS SECURELY in production (e.g., env variable)
# For development, we can generate/load it from a file.
encryption_key_str = os.environ.get('ENCRYPTION_KEY')
if not encryption_key_str:
logger.error("ENCRYPTION_KEY environment variable not set! RMS password encryption will fail.")
# Можно либо упасть с ошибкой, либо использовать временный ключ (НЕ РЕКОМЕНДУЕТСЯ для продакшена)
# raise ValueError("ENCRYPTION_KEY environment variable is required.")
# Для локального запуска без установки переменной, можно временно сгенерировать:
logger.warning("Generating temporary encryption key. SET ENCRYPTION_KEY ENV VAR FOR PRODUCTION!")
ENCRYPTION_KEY = Fernet.generate_key()
else:
try:
ENCRYPTION_KEY = encryption_key_str.encode('utf-8')
# Простая проверка, что ключ валидный для Fernet
Fernet(ENCRYPTION_KEY)
logger.info("Successfully loaded ENCRYPTION_KEY from environment variable.")
except Exception as e:
logger.error(f"Invalid ENCRYPTION_KEY format in environment variable: {e}")
raise ValueError("Invalid ENCRYPTION_KEY format.") from e
# 2. Инициализация Fernet вынесена в функцию, чтобы она вызывалась ПОСЛЕ загрузки .env
fernet = None
def init_encryption(app):
"""Инициализирует Fernet после того, как конфигурация загружена."""
global fernet
encryption_key_str = app.config.get('ENCRYPTION_KEY')
if not encryption_key_str:
logger.error("Переменная окружения ENCRYPTION_KEY не установлена! Шифрование паролей RMS не будет работать.")
logger.warning("Генерируется временный ключ шифрования. Для продакшена ОБЯЗАТЕЛЬНО установите ENCRYPTION_KEY!")
encryption_key = Fernet.generate_key()
else:
try:
encryption_key = encryption_key_str.encode('utf-8')
Fernet(encryption_key)
logger.info("Ключ шифрования ENCRYPTION_KEY успешно загружен.")
except Exception as e:
logger.critical(f"Недопустимый формат ключа ENCRYPTION_KEY: {e}")
raise ValueError("Недопустимый формат ключа ENCRYPTION_KEY.") from e
fernet = Fernet(encryption_key)
fernet = Fernet(ENCRYPTION_KEY)
class User(db.Model, UserMixin):
# ... код класса User остается без изменений ...
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
password_hash = db.Column(db.String(256))
config = db.relationship('UserConfig', backref='user', uselist=False, cascade="all, delete-orphan")
def set_password(self, password):
@@ -47,81 +49,73 @@ class User(db.Model, UserMixin):
def __repr__(self):
return f'<User {self.username}>'
class UserConfig(db.Model):
# ... код класса UserConfig почти без изменений ...
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True)
# RMS Config
rms_host = db.Column(db.String(200))
rms_login = db.Column(db.String(100))
rms_password_encrypted = db.Column(db.LargeBinary) # Store encrypted password
# Google Config
google_cred_file_path = db.Column(db.String(300)) # Store path, not content
rms_password_encrypted = db.Column(db.LargeBinary)
google_cred_file_path = db.Column(db.String(300))
google_sheet_url = db.Column(db.String(300))
google_client_email = db.Column(db.String(200)) # Store for display
# Mappings, Presets, Sheets (Stored as JSON strings)
mappings_json = db.Column(db.Text, default='{}')
google_client_email = db.Column(db.String(200))
presets_json = db.Column(db.Text, default='[]')
sheets_json = db.Column(db.Text, default='[]')
# --- Helper properties for easy access ---
mappings_json = db.Column(db.Text, default='{}')
@property
def rms_password(self):
"""Дешифрует пароль RMS при доступе."""
if not fernet:
raise RuntimeError("Fernet encryption is not initialized. Call init_encryption(app) first.")
if self.rms_password_encrypted:
try:
return fernet.decrypt(self.rms_password_encrypted).decode('utf-8')
except Exception: # Handle potential decryption errors
except Exception as e:
logger.error(f"Ошибка дешифрования пароля для user_id {self.user_id}: {e}")
return None
return None
@rms_password.setter
def rms_password(self, value):
"""Шифрует пароль RMS при установке."""
if not fernet:
raise RuntimeError("Fernet encryption is not initialized. Call init_encryption(app) first.")
if value:
self.rms_password_encrypted = fernet.encrypt(value.encode('utf-8'))
else:
self.rms_password_encrypted = None # Or handle as needed
@property
def mappings(self):
return json.loads(self.mappings_json or '{}')
@mappings.setter
def mappings(self, value):
self.mappings_json = json.dumps(value or {}, ensure_ascii=False)
self.rms_password_encrypted = None
# ... остальные properties (presets, sheets, mappings) и методы (get_rms_dict, get_google_dict) остаются без изменений ...
@property
def presets(self):
return json.loads(self.presets_json or '[]')
@presets.setter
def presets(self, value):
self.presets_json = json.dumps(value or [], ensure_ascii=False)
@property
def sheets(self):
return json.loads(self.sheets_json or '[]')
@sheets.setter
def sheets(self, value):
self.sheets_json = json.dumps(value or [], ensure_ascii=False)
# Convenience getter for template display
@property
def mappings(self):
return json.loads(self.mappings_json or '{}')
@mappings.setter
def mappings(self, value):
self.mappings_json = json.dumps(value or {}, ensure_ascii=False)
def get_rms_dict(self):
return {
'host': self.rms_host or '',
'login': self.rms_login or '',
'password': self.rms_password or '' # Use decrypted password here if needed for display/form population (be cautious!)
# Usually, password fields are left blank in forms for security.
'password_is_set': bool(self.rms_password_encrypted)
}
def get_google_dict(self):
return {
'cred_file': self.google_cred_file_path or '', # Maybe just indicate if file exists?
return {
'cred_file_is_set': bool(self.google_cred_file_path and os.path.exists(self.google_cred_file_path)),
'sheet_url': self.google_sheet_url or ''
}
}
def __repr__(self):
return f'<UserConfig for User ID {self.user_id}>'