feat(phase5): integracion auth hibrida, email dinamico y moodle 4.1 backend con tolerancia a fallos
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
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), flags globales y metadatos.
|
||||
Permite almacenar enlaces de integración (e.g. Google Sheets, Moodle),
|
||||
flags globales, credenciales SMTP encriptadas y secretos OAuth.
|
||||
"""
|
||||
__tablename__ = 'system_settings'
|
||||
|
||||
@@ -12,10 +14,13 @@ class SystemSetting(db.Model):
|
||||
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() != '':
|
||||
@@ -24,27 +29,103 @@ class SystemSetting(db.Model):
|
||||
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):
|
||||
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=value, description=description)
|
||||
record = cls(
|
||||
key=key,
|
||||
value=final_value,
|
||||
description=description,
|
||||
category=category,
|
||||
is_encrypted=is_encrypted
|
||||
)
|
||||
db.session.add(record)
|
||||
else:
|
||||
record.value = value
|
||||
# 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
|
||||
|
||||
def to_dict(self):
|
||||
@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': self.value,
|
||||
'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}={self.value}>'
|
||||
return f'<SystemSetting {self.key}>'
|
||||
|
||||
Reference in New Issue
Block a user