"""Settings API routes.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db, cache from shopdb.core.models import Setting, AuditLog from shopdb.utils.responses import success_response, error_response, ErrorCodes settings_bp = Blueprint('settings', __name__) # Cache key for settings SETTINGS_CACHE_KEY = 'system_settings' SETTINGS_CACHE_TTL = 300 # 5 minutes def get_cached_settings(): """Get all settings from cache or database.""" cached = cache.get(SETTINGS_CACHE_KEY) if cached is not None: return cached settings = Setting.query.all() result = {s.key: s.get_typed_value() for s in settings} cache.set(SETTINGS_CACHE_KEY, result, timeout=SETTINGS_CACHE_TTL) return result def invalidate_settings_cache(): """Clear the settings cache.""" cache.delete(SETTINGS_CACHE_KEY) @settings_bp.route('', methods=['GET']) @jwt_required(optional=True) def list_settings(): """List all settings, optionally filtered by category.""" category = request.args.get('category') query = Setting.query if category: query = query.filter_by(category=category) settings = query.order_by(Setting.category, Setting.key).all() return success_response([s.to_dict() for s in settings]) @settings_bp.route('/', methods=['GET']) @jwt_required(optional=True) def get_setting(key: str): """Get a single setting by key.""" setting = Setting.query.filter_by(key=key).first() if not setting: return error_response(ErrorCodes.NOT_FOUND, f'Setting {key} not found', http_code=404) return success_response(setting.to_dict()) @settings_bp.route('/', methods=['PUT']) @jwt_required() def update_setting(key: str): """Update a setting value.""" data = request.get_json() if data is None or 'value' not in data: return error_response(ErrorCodes.VALIDATION_ERROR, 'value is required') setting = Setting.query.filter_by(key=key).first() if not setting: return error_response(ErrorCodes.NOT_FOUND, f'Setting {key} not found', http_code=404) # Track old value for audit old_value = setting.value # Convert value to string for storage value = data['value'] if isinstance(value, bool): setting.value = 'true' if value else 'false' else: setting.value = str(value) if value is not None else None # Audit log (mask sensitive values) is_sensitive = 'password' in key or 'token' in key or 'secret' in key AuditLog.log('updated', 'Setting', entityname=key, changes={ 'value': { 'old': '***' if is_sensitive else old_value, 'new': '***' if is_sensitive else setting.value } }) db.session.commit() invalidate_settings_cache() return success_response(setting.to_dict(), message='Setting updated') @settings_bp.route('', methods=['POST']) @jwt_required() def create_setting(): """Create a new setting (admin only).""" data = request.get_json() if not data or not data.get('key'): return error_response(ErrorCodes.VALIDATION_ERROR, 'key is required') if Setting.query.filter_by(key=data['key']).first(): return error_response(ErrorCodes.CONFLICT, f"Setting '{data['key']}' already exists", http_code=409) value = data.get('value') if isinstance(value, bool): value_str = 'true' if value else 'false' else: value_str = str(value) if value is not None else None setting = Setting( key=data['key'], value=value_str, valuetype=data.get('valuetype', 'string'), category=data.get('category', 'general'), description=data.get('description') ) db.session.add(setting) db.session.commit() invalidate_settings_cache() return success_response(setting.to_dict(), message='Setting created', http_code=201) @settings_bp.route('/seed', methods=['POST']) @jwt_required() def seed_default_settings(): """Seed default settings if they don't exist.""" defaults = [ # Zabbix integration { 'key': 'zabbix_enabled', 'value': 'false', 'valuetype': 'boolean', 'category': 'integrations', 'description': 'Enable Zabbix integration for printer supply monitoring' }, { 'key': 'zabbix_url', 'value': '', 'valuetype': 'string', 'category': 'integrations', 'description': 'Zabbix API URL (e.g., http://zabbix.example.com:8080)' }, { 'key': 'zabbix_token', 'value': '', 'valuetype': 'string', 'category': 'integrations', 'description': 'Zabbix API authentication token' }, # Email/SMTP settings { 'key': 'smtp_enabled', 'value': 'false', 'valuetype': 'boolean', 'category': 'email', 'description': 'Enable email notifications and alerts' }, { 'key': 'smtp_host', 'value': '', 'valuetype': 'string', 'category': 'email', 'description': 'SMTP server hostname' }, { 'key': 'smtp_port', 'value': '587', 'valuetype': 'integer', 'category': 'email', 'description': 'SMTP server port (usually 587 for TLS, 465 for SSL, 25 for unencrypted)' }, { 'key': 'smtp_username', 'value': '', 'valuetype': 'string', 'category': 'email', 'description': 'SMTP authentication username' }, { 'key': 'smtp_password', 'value': '', 'valuetype': 'string', 'category': 'email', 'description': 'SMTP authentication password' }, { 'key': 'smtp_use_tls', 'value': 'true', 'valuetype': 'boolean', 'category': 'email', 'description': 'Use TLS encryption for SMTP connection' }, { 'key': 'smtp_from_address', 'value': '', 'valuetype': 'string', 'category': 'email', 'description': 'From address for outgoing emails' }, { 'key': 'smtp_from_name', 'value': 'ShopDB', 'valuetype': 'string', 'category': 'email', 'description': 'From name for outgoing emails' }, { 'key': 'alert_recipients', 'value': '', 'valuetype': 'string', 'category': 'email', 'description': 'Default email recipients for alerts (comma-separated)' }, # Audit log settings { 'key': 'audit_retention_days', 'value': '90', 'valuetype': 'integer', 'category': 'audit', 'description': 'Number of days to retain audit logs (0 = keep forever)' }, # Authentication settings { 'key': 'saml_enabled', 'value': 'false', 'valuetype': 'boolean', 'category': 'auth', 'description': 'Enable SAML SSO authentication' }, { 'key': 'saml_idp_metadata_url', 'value': '', 'valuetype': 'string', 'category': 'auth', 'description': 'SAML Identity Provider metadata URL' }, { 'key': 'saml_entity_id', 'value': '', 'valuetype': 'string', 'category': 'auth', 'description': 'SAML Service Provider entity ID (e.g., https://shopdb.example.com)' }, { 'key': 'saml_acs_url', 'value': '', 'valuetype': 'string', 'category': 'auth', 'description': 'SAML Assertion Consumer Service URL' }, { 'key': 'saml_allow_local_login', 'value': 'true', 'valuetype': 'boolean', 'category': 'auth', 'description': 'Allow local username/password login when SAML is enabled' }, { 'key': 'saml_auto_create_users', 'value': 'true', 'valuetype': 'boolean', 'category': 'auth', 'description': 'Automatically create users on first SAML login' }, { 'key': 'saml_admin_group', 'value': '', 'valuetype': 'string', 'category': 'auth', 'description': 'SAML group name that grants admin role' }, ] created = 0 for d in defaults: if not Setting.query.filter_by(key=d['key']).first(): setting = Setting(**d) db.session.add(setting) created += 1 db.session.commit() invalidate_settings_cache() return success_response({'created': created}, message=f'{created} default settings created')