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''