Files
mcphub/core/settings.py
airano-ir 43fd2201a0 feat(v3.13.0): settings live DB reads, remove Directus/Appwrite, admin stats
Settings fixes:
- MAX_SITES_PER_USER, USER_RATE_LIMIT_PER_MIN/HR now read from DB settings
  table (DB > ENV > default), so dashboard/settings changes apply without
  restart. Sync cache refreshed on every save or delete.
- /api/me reports the live DB value for max_sites_per_user.

Admin improvements:
- Admin users bypass per-user rate limiting entirely (role=admin or ADMIN_EMAILS).
- Admin Overview now shows platform stats: registered users, new users (7d),
  total user sites, available tools.

Plugin cleanup:
- Appwrite and Directus plugins removed from the active registry (8 plugins
  now: WordPress, WooCommerce, WordPress Specialist, Gitea, n8n, Supabase,
  OpenPanel, Coolify). Plugin code is retained for future re-enabling.
- Settings page plugin visibility list updated to match.

Mobile onboarding:
- Stepper steps on narrow viewports stack vertically with correct full border
  and rounded corners on each step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:33:20 +02:00

231 lines
7.5 KiB
Python

"""Unified settings access with DB > ENV > Default priority (Phase 4C.3).
Usage:
from core.settings import get_setting
enabled = await get_setting(
"ENABLED_PLUGINS",
"wordpress,woocommerce,wordpress_specialist,supabase,openpanel,gitea,n8n,coolify",
)
max_sites = int(await get_setting("MAX_SITES_PER_USER", "10"))
"""
import logging
import os
logger = logging.getLogger(__name__)
# Cached plugin set for sync access (updated when settings change)
_cached_plugins: set[str] | None = None
# Cached int settings for sync access (updated when settings change)
_cached_max_sites: int | None = None
_cached_rate_per_min: int | None = None
_cached_rate_per_hr: int | None = None
# Default values for all managed settings
SETTING_DEFAULTS: dict[str, str] = {
"ENABLED_PLUGINS": "wordpress,woocommerce,wordpress_specialist,supabase,openpanel,gitea,n8n,coolify",
"MAX_SITES_PER_USER": "10",
"USER_RATE_LIMIT_PER_MIN": "30",
"USER_RATE_LIMIT_PER_HR": "500",
}
# Human-readable labels for the settings UI
SETTING_LABELS: dict[str, dict[str, str]] = {
"ENABLED_PLUGINS": {
"label": "Enabled Plugins",
"label_fa": "پلاگین‌های فعال",
"hint": "Comma-separated plugin types visible to public users",
"hint_fa": "انواع پلاگین قابل مشاهده برای کاربران عمومی (با کاما جدا شوند)",
},
"MAX_SITES_PER_USER": {
"label": "Max Sites per User",
"label_fa": "حداکثر سایت هر کاربر",
"hint": "Maximum number of sites each user can create",
"hint_fa": "حداکثر تعداد سایت‌هایی که هر کاربر می‌تواند بسازد",
},
"USER_RATE_LIMIT_PER_MIN": {
"label": "User Rate Limit (per minute)",
"label_fa": "محدودیت نرخ کاربر (در دقیقه)",
"hint": "Maximum MCP requests per user per minute",
"hint_fa": "حداکثر درخواست MCP هر کاربر در دقیقه",
},
"USER_RATE_LIMIT_PER_HR": {
"label": "User Rate Limit (per hour)",
"label_fa": "محدودیت نرخ کاربر (در ساعت)",
"hint": "Maximum MCP requests per user per hour",
"hint_fa": "حداکثر درخواست MCP هر کاربر در ساعت",
},
}
async def get_setting(key: str, default: str | None = None) -> str | None:
"""Get a setting value with priority: Database > Environment > Default.
Args:
key: Setting key (e.g., "ENABLED_PLUGINS").
default: Fallback if not found anywhere. If None, uses SETTING_DEFAULTS.
Returns:
Setting value string, or None if not found anywhere.
"""
# 1. Try database
try:
from core.database import get_database
db = get_database()
db_val = await db.get_setting(key)
if db_val is not None:
return db_val
except Exception:
pass # DB not initialized yet (startup) — fall through
# 2. Try environment variable
env_val = os.environ.get(key)
if env_val is not None:
return env_val
# 3. Use provided default or SETTING_DEFAULTS
if default is not None:
return default
return SETTING_DEFAULTS.get(key)
def get_cached_max_sites() -> int:
"""Return the cached MAX_SITES_PER_USER value (sync, uses last refresh)."""
if _cached_max_sites is not None:
return _cached_max_sites
env_val = os.environ.get("MAX_SITES_PER_USER")
if env_val:
try:
return max(1, int(env_val))
except ValueError:
pass
return int(SETTING_DEFAULTS["MAX_SITES_PER_USER"])
def get_cached_rate_per_min() -> int:
"""Return the cached USER_RATE_LIMIT_PER_MIN value (sync, uses last refresh)."""
if _cached_rate_per_min is not None:
return _cached_rate_per_min
env_val = os.environ.get("USER_RATE_LIMIT_PER_MIN")
if env_val:
try:
return max(1, int(env_val))
except ValueError:
pass
return int(SETTING_DEFAULTS["USER_RATE_LIMIT_PER_MIN"])
def get_cached_rate_per_hr() -> int:
"""Return the cached USER_RATE_LIMIT_PER_HR value (sync, uses last refresh)."""
if _cached_rate_per_hr is not None:
return _cached_rate_per_hr
env_val = os.environ.get("USER_RATE_LIMIT_PER_HR")
if env_val:
try:
return max(1, int(env_val))
except ValueError:
pass
return int(SETTING_DEFAULTS["USER_RATE_LIMIT_PER_HR"])
async def refresh_plugin_cache() -> None:
"""Refresh all sync-readable setting caches from DB/ENV/default."""
global _cached_plugins, _cached_max_sites, _cached_rate_per_min, _cached_rate_per_hr
val = await get_setting("ENABLED_PLUGINS")
if val:
_cached_plugins = {p.strip().lower() for p in val.split(",") if p.strip()}
else:
_cached_plugins = None
try:
raw = await get_setting("MAX_SITES_PER_USER")
_cached_max_sites = max(1, int(raw)) if raw else int(SETTING_DEFAULTS["MAX_SITES_PER_USER"])
except (ValueError, TypeError):
_cached_max_sites = int(SETTING_DEFAULTS["MAX_SITES_PER_USER"])
try:
raw = await get_setting("USER_RATE_LIMIT_PER_MIN")
_cached_rate_per_min = (
max(1, int(raw)) if raw else int(SETTING_DEFAULTS["USER_RATE_LIMIT_PER_MIN"])
)
except (ValueError, TypeError):
_cached_rate_per_min = int(SETTING_DEFAULTS["USER_RATE_LIMIT_PER_MIN"])
try:
raw = await get_setting("USER_RATE_LIMIT_PER_HR")
_cached_rate_per_hr = (
max(1, int(raw)) if raw else int(SETTING_DEFAULTS["USER_RATE_LIMIT_PER_HR"])
)
except (ValueError, TypeError):
_cached_rate_per_hr = int(SETTING_DEFAULTS["USER_RATE_LIMIT_PER_HR"])
async def save_setting(key: str, value: str) -> None:
"""Save a setting to database and refresh caches."""
from core.database import get_database
db = get_database()
await db.set_setting(key, value)
await refresh_plugin_cache()
async def delete_setting_value(key: str) -> bool:
"""Delete a setting from database (revert to ENV/default)."""
from core.database import get_database
db = get_database()
deleted = await db.delete_setting(key)
await refresh_plugin_cache()
return deleted
async def get_all_managed_settings() -> list[dict[str, str]]:
"""Get all managed settings with their current values and sources.
Returns:
List of dicts with: key, value, source ("database"/"environment"/"default"),
label, hint.
"""
result = []
try:
from core.database import get_database
db = get_database()
db_settings = await db.get_all_settings()
except Exception:
db_settings = {}
for key, default_val in SETTING_DEFAULTS.items():
meta = SETTING_LABELS.get(key, {})
db_val = db_settings.get(key)
env_val = os.environ.get(key)
if db_val is not None:
value, source = db_val, "database"
elif env_val is not None:
value, source = env_val, "environment"
else:
value, source = default_val, "default"
result.append(
{
"key": key,
"value": value,
"source": source,
"default": default_val,
"label": meta.get("label", key),
"label_fa": meta.get("label_fa", key),
"hint": meta.get("hint", ""),
"hint_fa": meta.get("hint_fa", ""),
}
)
return result