Files

132 lines
5.7 KiB
Python

from app import db
from datetime import datetime
from app.services.crypto_service import CryptoService
class SystemSetting(db.Model):
"""
Parámetros de configuración del sistema persistentes en base de datos.
Permite almacenar enlaces de integración (e.g. Google Sheets, Moodle),
flags globales, credenciales SMTP encriptadas y secretos OAuth.
"""
__tablename__ = 'system_settings'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False, index=True)
value = db.Column(db.Text, nullable=True)
description = db.Column(db.String(255), nullable=True)
category = db.Column(db.String(50), default='system', nullable=True)
is_encrypted = db.Column(db.Boolean, default=False, nullable=True)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@classmethod
def get_value(cls, key, default=None):
"""Retorna el valor tal como está almacenado en BD."""
try:
record = cls.query.filter_by(key=key).first()
if record and record.value is not None and record.value.strip() != '':
return record.value
except Exception:
pass
return default
def get_decrypted_value(self_or_cls, key_or_default=None, default=None):
"""Retorna el valor desencriptado. Compatible como método de clase o de instancia."""
if isinstance(self_or_cls, type):
# Llamado como clase: SystemSetting.get_decrypted_value('key', default)
key = key_or_default
try:
record = self_or_cls.query.filter_by(key=key).first()
if record:
return record.get_decrypted_value(default)
except Exception:
pass
return default
else:
# Llamado como instancia: setting.get_decrypted_value(default)
actual_default = key_or_default
try:
if self_or_cls.value is not None and self_or_cls.value.strip() != '':
if self_or_cls.is_encrypted:
return CryptoService.decrypt(self_or_cls.value)
return self_or_cls.value
except Exception:
pass
return actual_default
def get_masked_value(self_or_cls, key_or_default=None, default=''):
"""Retorna una versión enmascarada si es un secreto o contraseña. Compatible como clase o instancia."""
if isinstance(self_or_cls, type):
# Llamado como clase: SystemSetting.get_masked_value('key', default)
key = key_or_default
try:
record = self_or_cls.query.filter_by(key=key).first()
if record:
return record.get_masked_value(default)
except Exception:
pass
return default
else:
# Llamado como instancia: setting.get_masked_value(default)
actual_default = key_or_default or ''
try:
if self_or_cls.value and self_or_cls.value.strip() != '':
if self_or_cls.is_encrypted or 'secret' in self_or_cls.key.lower() or 'password' in self_or_cls.key.lower() or 'token' in self_or_cls.key.lower():
return '••••••••••••'
return self_or_cls.value
except Exception:
pass
return actual_default
@classmethod
def set_value(cls, key, value, description=None, category='system', is_encrypted=False):
"""Guarda o actualiza un parámetro de configuración."""
record = cls.query.filter_by(key=key).first()
final_value = value
if is_encrypted and value:
# Si el valor ya viene cifrado o es un placeholder de no-cambio '••••••••••••', no recifrar
if value != '••••••••••••':
final_value = CryptoService.encrypt(value)
if not record:
record = cls(
key=key,
value=final_value,
description=description,
category=category,
is_encrypted=is_encrypted
)
db.session.add(record)
else:
# Si el valor ingresado es el placeholder, no sobrescribir la contraseña existente
if not (is_encrypted and value == '••••••••••••'):
record.value = final_value
if description:
record.description = description
if category:
record.category = category
record.is_encrypted = is_encrypted
record.updated_at = datetime.utcnow()
db.session.commit()
return record
@classmethod
def set_encrypted_value(cls, key, value, description=None, category='system'):
"""Helper para guardar directamente valores sensibles cifrados."""
return cls.set_value(key, value, description=description, category=category, is_encrypted=True)
def to_dict(self, mask_secrets=True):
val = self.value
if mask_secrets and (self.is_encrypted or 'password' in self.key.lower() or 'secret' in self.key.lower() or 'token' in self.key.lower()):
val = '••••••••••••' if val else ''
return {
'key': self.key,
'value': val,
'description': self.description,
'category': self.category or 'system',
'is_encrypted': bool(self.is_encrypted),
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return f'<SystemSetting {self.key}>'