Files
app-service/backend/app/core/settings_registry.py
matteoscrugli 8c4a555b88 Add comprehensive backend features and mobile UI improvements
Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:27:32 +01:00

664 lines
20 KiB
Python

"""
Settings Registry - Central definition of all application settings.
This file defines ALL configurable settings in the application with:
- Storage location (database in /config, localStorage, env)
- Scope (global or user-specific)
- Default values
- Data types
- Descriptions
IMPORTANT: Any new setting MUST be added here first.
"""
from enum import Enum
from dataclasses import dataclass, field
from typing import Any, Optional
class SettingScope(Enum):
"""Defines who the setting applies to."""
GLOBAL = "global" # Same for all users, admin-controlled
USER_SPECIFIC = "user" # Each user can have different value
SYSTEM = "system" # Infrastructure/env settings, not in DB
class SettingStorage(Enum):
"""Defines where the setting is stored."""
DATABASE = "database" # Stored in /config/config.db (settings table)
LOCAL_STORAGE = "localStorage" # Browser localStorage (frontend only)
ENV = "env" # Environment variable (.env file)
MEMORY = "memory" # Runtime only, not persisted
class SettingType(Enum):
"""Data type of the setting value."""
BOOLEAN = "boolean"
STRING = "string"
INTEGER = "integer"
JSON = "json"
LIST = "list"
@dataclass
class SettingDefinition:
"""Definition of a single setting."""
key: str
type: SettingType
scope: SettingScope
storage: SettingStorage
default: Any
description: str
category: str
admin_only: bool = True # Only admins can modify (for DB settings)
sync_to_frontend: bool = True # Should be synced to frontend
choices: Optional[list] = None # Valid values if restricted
# =============================================================================
# SETTINGS REGISTRY
# =============================================================================
SETTINGS_REGISTRY: dict[str, SettingDefinition] = {}
def register_setting(setting: SettingDefinition) -> SettingDefinition:
"""Register a setting in the registry."""
SETTINGS_REGISTRY[setting.key] = setting
return setting
# =============================================================================
# THEME SETTINGS (Global, Database)
# =============================================================================
register_setting(SettingDefinition(
key="theme_accent_color",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="auto",
description="Accent color for UI elements",
category="theme",
choices=["auto", "blue", "purple", "green", "orange", "pink", "red", "teal", "amber", "indigo", "cyan", "rose"]
))
register_setting(SettingDefinition(
key="theme_border_radius",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="large",
description="Border radius style for UI components",
category="theme",
choices=["small", "medium", "large"]
))
register_setting(SettingDefinition(
key="theme_sidebar_style",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="default",
description="Visual style of the sidebar",
category="theme",
choices=["default", "dark", "light"]
))
register_setting(SettingDefinition(
key="theme_density",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="compact",
description="UI density/spacing",
category="theme",
choices=["compact", "comfortable", "spacious"]
))
register_setting(SettingDefinition(
key="theme_font_family",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="sans",
description="Font family for the application",
category="theme",
choices=["sans", "inter", "roboto"]
))
register_setting(SettingDefinition(
key="theme_color_palette",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="monochrome",
description="Color palette preset",
category="theme",
choices=["default", "monochrome", "monochromeBlue", "sepia", "nord", "dracula", "solarized", "github", "ocean", "forest", "midnight", "sunset"]
))
register_setting(SettingDefinition(
key="theme_custom_colors",
type=SettingType.JSON,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="{}",
description="Custom color overrides as JSON",
category="theme"
))
register_setting(SettingDefinition(
key="theme_dark_mode_location",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="sidebar",
description="Where to show dark mode toggle",
category="theme",
choices=["sidebar", "user_menu"]
))
register_setting(SettingDefinition(
key="theme_language_location",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="sidebar",
description="Where to show language toggle",
category="theme",
choices=["sidebar", "user_menu"]
))
register_setting(SettingDefinition(
key="theme_show_dark_mode_toggle",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Show/hide dark mode toggle globally",
category="theme"
))
register_setting(SettingDefinition(
key="theme_show_language_toggle",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=False,
description="Show/hide language toggle globally",
category="theme"
))
register_setting(SettingDefinition(
key="theme_show_dark_mode_login",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Show dark mode toggle on login page",
category="theme"
))
register_setting(SettingDefinition(
key="theme_show_language_login",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=False,
description="Show language toggle on login page",
category="theme"
))
# =============================================================================
# MODULE/FEATURE SETTINGS (Global, Database)
# =============================================================================
register_setting(SettingDefinition(
key="module_feature1_admin_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Enable Feature 1 module for admin users",
category="modules"
))
register_setting(SettingDefinition(
key="module_feature1_user_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Enable Feature 1 module for regular users",
category="modules"
))
register_setting(SettingDefinition(
key="module_feature2_admin_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Enable Feature 2 module for admin users",
category="modules"
))
register_setting(SettingDefinition(
key="module_feature2_user_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Enable Feature 2 module for regular users",
category="modules"
))
register_setting(SettingDefinition(
key="module_feature3_admin_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Enable Feature 3 module for admin users",
category="modules"
))
register_setting(SettingDefinition(
key="module_feature3_user_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Enable Feature 3 module for regular users",
category="modules"
))
register_setting(SettingDefinition(
key="modules_order",
type=SettingType.LIST,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=["feature1", "feature2", "feature3"],
description="Order of feature modules in sidebar",
category="modules"
))
# =============================================================================
# AUTHENTICATION & SECURITY SETTINGS (Global, Database)
# =============================================================================
register_setting(SettingDefinition(
key="registration_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Allow new user registration",
category="auth"
))
register_setting(SettingDefinition(
key="user_mode_enabled",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=False,
description="Enable admin/user view mode switching",
category="auth"
))
# Password policy settings
register_setting(SettingDefinition(
key="password_min_length",
type=SettingType.INTEGER,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=8,
description="Minimum password length",
category="security"
))
register_setting(SettingDefinition(
key="password_require_uppercase",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Require uppercase letters in passwords",
category="security"
))
register_setting(SettingDefinition(
key="password_require_lowercase",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Require lowercase letters in passwords",
category="security"
))
register_setting(SettingDefinition(
key="password_require_digit",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Require digits in passwords",
category="security"
))
register_setting(SettingDefinition(
key="password_require_special",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=False,
description="Require special characters in passwords",
category="security"
))
# =============================================================================
# UI/LAYOUT SETTINGS (Global, Database)
# =============================================================================
register_setting(SettingDefinition(
key="sidebar_mode",
type=SettingType.STRING,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default="toggle",
description="Sidebar behavior mode",
category="ui",
choices=["collapsed", "expanded", "toggle", "dynamic"]
))
register_setting(SettingDefinition(
key="show_logo",
type=SettingType.BOOLEAN,
scope=SettingScope.GLOBAL,
storage=SettingStorage.DATABASE,
default=True,
description="Show logo in sidebar instead of text",
category="ui"
))
# =============================================================================
# USER-SPECIFIC SETTINGS (Frontend localStorage)
# These settings are managed by the frontend per-browser/user-session.
# If you need cross-device persistence, implement a per-user DB table + API.
# =============================================================================
register_setting(SettingDefinition(
key="user_theme_mode",
type=SettingType.STRING,
scope=SettingScope.USER_SPECIFIC,
storage=SettingStorage.LOCAL_STORAGE,
default="system",
description="User's preferred theme mode (light/dark/system)",
category="user_preferences",
admin_only=False,
choices=["light", "dark", "system"]
))
register_setting(SettingDefinition(
key="user_language",
type=SettingType.STRING,
scope=SettingScope.USER_SPECIFIC,
storage=SettingStorage.LOCAL_STORAGE,
default="en",
description="User's preferred language",
category="user_preferences",
admin_only=False,
choices=["en", "it"]
))
register_setting(SettingDefinition(
key="user_sidebar_collapsed",
type=SettingType.BOOLEAN,
scope=SettingScope.USER_SPECIFIC,
storage=SettingStorage.LOCAL_STORAGE,
default=False,
description="User's sidebar collapsed state preference",
category="user_preferences",
admin_only=False
))
register_setting(SettingDefinition(
key="user_view_mode",
type=SettingType.STRING,
scope=SettingScope.USER_SPECIFIC,
storage=SettingStorage.LOCAL_STORAGE,
default="admin",
description="User's current view mode (admin/user)",
category="user_preferences",
admin_only=False,
choices=["admin", "user"]
))
# =============================================================================
# SYSTEM/ENVIRONMENT SETTINGS (Infrastructure, .env file)
# These are NOT stored in the database
# =============================================================================
register_setting(SettingDefinition(
key="SECRET_KEY",
type=SettingType.STRING,
scope=SettingScope.SYSTEM,
storage=SettingStorage.ENV,
default=None, # Required, no default
description="Secret key for JWT token signing",
category="security",
sync_to_frontend=False
))
register_setting(SettingDefinition(
key="DATABASE_URL",
type=SettingType.STRING,
scope=SettingScope.SYSTEM,
storage=SettingStorage.ENV,
default="sqlite:////config/config.db",
description="Database connection URL",
category="infrastructure",
sync_to_frontend=False
))
register_setting(SettingDefinition(
key="ALGORITHM",
type=SettingType.STRING,
scope=SettingScope.SYSTEM,
storage=SettingStorage.ENV,
default="HS256",
description="JWT signing algorithm",
category="security",
sync_to_frontend=False
))
register_setting(SettingDefinition(
key="ACCESS_TOKEN_EXPIRE_MINUTES",
type=SettingType.INTEGER,
scope=SettingScope.SYSTEM,
storage=SettingStorage.ENV,
default=1440,
description="JWT token expiration time in minutes",
category="security",
sync_to_frontend=False
))
register_setting(SettingDefinition(
key="ALLOWED_HOSTS",
type=SettingType.LIST,
scope=SettingScope.SYSTEM,
storage=SettingStorage.ENV,
default=[],
description="Additional allowed CORS origins",
category="security",
sync_to_frontend=False
))
register_setting(SettingDefinition(
key="LOG_LEVEL",
type=SettingType.STRING,
scope=SettingScope.SYSTEM,
storage=SettingStorage.ENV,
default="info",
description="Application logging level",
category="infrastructure",
choices=["debug", "info", "warning", "error", "critical"],
sync_to_frontend=False
))
register_setting(SettingDefinition(
key="DEBUG",
type=SettingType.BOOLEAN,
scope=SettingScope.SYSTEM,
storage=SettingStorage.ENV,
default=False,
description="Enable debug mode",
category="infrastructure",
sync_to_frontend=False
))
# =============================================================================
# FRONTEND-ONLY SETTINGS (localStorage, not in database)
# These are managed entirely by the frontend
# =============================================================================
register_setting(SettingDefinition(
key="token",
type=SettingType.STRING,
scope=SettingScope.USER_SPECIFIC,
storage=SettingStorage.LOCAL_STORAGE,
default=None,
description="JWT authentication token",
category="auth",
sync_to_frontend=False # Managed by frontend only
))
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
def get_settings_by_category(category: str) -> list[SettingDefinition]:
"""Get all settings in a specific category."""
return [s for s in SETTINGS_REGISTRY.values() if s.category == category]
def get_settings_by_scope(scope: SettingScope) -> list[SettingDefinition]:
"""Get all settings with a specific scope."""
return [s for s in SETTINGS_REGISTRY.values() if s.scope == scope]
def get_settings_by_storage(storage: SettingStorage) -> list[SettingDefinition]:
"""Get all settings stored in a specific location."""
return [s for s in SETTINGS_REGISTRY.values() if s.storage == storage]
def get_database_settings() -> list[SettingDefinition]:
"""Get all settings that should be stored in the database."""
return get_settings_by_storage(SettingStorage.DATABASE)
def get_global_settings() -> list[SettingDefinition]:
"""Get all global (non user-specific) settings."""
return get_settings_by_scope(SettingScope.GLOBAL)
def get_user_specific_settings() -> list[SettingDefinition]:
"""Get all user-specific settings."""
return get_settings_by_scope(SettingScope.USER_SPECIFIC)
def get_default_value(key: str) -> Any:
"""Get the default value for a setting."""
if key in SETTINGS_REGISTRY:
return SETTINGS_REGISTRY[key].default
return None
def get_all_defaults() -> dict[str, Any]:
"""Get all default values as a dictionary."""
return {key: setting.default for key, setting in SETTINGS_REGISTRY.items()}
def get_database_defaults() -> dict[str, Any]:
"""Get default values for all database-stored settings."""
return {
setting.key: setting.default
for setting in get_database_settings()
}
def validate_setting_value(key: str, value: Any) -> bool:
"""Validate a setting value against its definition."""
if key not in SETTINGS_REGISTRY:
return False
setting = SETTINGS_REGISTRY[key]
# Check type
if setting.type == SettingType.BOOLEAN:
if not isinstance(value, bool) and value not in ['true', 'false', 'True', 'False']:
return False
elif setting.type == SettingType.INTEGER:
if not isinstance(value, int):
try:
int(value)
except (ValueError, TypeError):
return False
elif setting.type == SettingType.STRING:
if not isinstance(value, str):
return False
# Check choices if defined
if setting.choices and value not in setting.choices:
# For booleans converted to strings
if setting.type == SettingType.BOOLEAN:
return True
return False
return True
# =============================================================================
# CATEGORY KEYS (for API endpoints)
# =============================================================================
THEME_KEYS = [s.key for s in get_settings_by_category("theme")]
MODULE_KEYS = [s.key for s in get_settings_by_category("modules")]
AUTH_KEYS = [s.key for s in get_settings_by_category("auth")]
UI_KEYS = [s.key for s in get_settings_by_category("ui")]
USER_PREFERENCE_KEYS = [s.key for s in get_settings_by_category("user_preferences")]
# =============================================================================
# PRINT SUMMARY (for debugging)
# =============================================================================
if __name__ == "__main__":
print("\n=== SETTINGS REGISTRY SUMMARY ===\n")
print(f"Total settings: {len(SETTINGS_REGISTRY)}")
print(f" - Database settings: {len(get_database_settings())}")
print(f" - Global settings: {len(get_global_settings())}")
print(f" - User-specific settings: {len(get_user_specific_settings())}")
print("\n--- By Category ---")
categories = set(s.category for s in SETTINGS_REGISTRY.values())
for cat in sorted(categories):
settings = get_settings_by_category(cat)
print(f" {cat}: {len(settings)} settings")
print("\n--- Database Settings (stored in /config/config.db) ---")
for setting in get_database_settings():
scope_label = "GLOBAL" if setting.scope == SettingScope.GLOBAL else "USER"
print(f" [{scope_label}] {setting.key}: {setting.type.value} = {setting.default}")