From 5686ae5468581ff9a9e77b6cc6a5eaa28a7fef68 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 4 Jun 2026 20:46:13 +0300 Subject: [PATCH 1/3] fix(security): remove active weak default API key from shipped config default_config.yaml shipped api_keys.dev: "development-key-change-in-production" uncommitted/active, while the surrounding comment claimed it had been removed. On a non-loopback bind this is a publicly-known credential granting full LAN access. Restore the documented secure default (empty api_keys -> loopback-only anonymous, LAN rejected) and leave a commented example instead. --- server/config/default_config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/config/default_config.yaml b/server/config/default_config.yaml index daa46fc..e7e6a46 100644 --- a/server/config/default_config.yaml +++ b/server/config/default_config.yaml @@ -15,11 +15,11 @@ auth: # - LAN requests are REJECTED with 401 (security default) # To enable LAN access, uncomment the example below and replace the value # with a secret you generated yourself (e.g. `openssl rand -hex 32`). - # The previous default `dev: "development-key-change-in-production"` has - # been removed — it shipped as a publicly-known token and any deployment - # that still uses it grants full LAN access to anyone on the network. - api_keys: - dev: "development-key-change-in-production" + # Do NOT ship a hard-coded key here — a publicly-known token grants full + # LAN access to anyone on the network. + api_keys: {} + # api_keys: + # my-client: "replace-with-output-of-openssl-rand-hex-32" # Storage paths default to ./data relative to the server's working directory. # Set LEDGRAB_DATA_DIR in the environment to point at a different data root From fdc9201660da185428f030211e304e41cd2802a6 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 4 Jun 2026 20:46:24 +0300 Subject: [PATCH 2/3] fix(api): remove broken legacy /system/mqtt/settings route The GET/PUT /api/v1/system/mqtt/settings handlers read cfg.mqtt.*, but the single-broker MQTTConfig block was removed in the multi-broker refactor, so any call raised AttributeError. Brokers are now first-class MQTTSource entities managed via the mqtt.py router, and the frontend no longer calls this endpoint. Remove the dead handlers, the _load_mqtt_settings helper, the now-unused get_config import, and the orphaned MQTTSettings{Request,Response} schemas. --- .../src/ledgrab/api/routes/system_settings.py | 84 +------------------ server/src/ledgrab/api/schemas/system.py | 29 ------- 2 files changed, 1 insertion(+), 112 deletions(-) diff --git a/server/src/ledgrab/api/routes/system_settings.py b/server/src/ledgrab/api/routes/system_settings.py index 172a37d..a3edd9a 100644 --- a/server/src/ledgrab/api/routes/system_settings.py +++ b/server/src/ledgrab/api/routes/system_settings.py @@ -1,4 +1,4 @@ -"""System routes: MQTT, external URL, ADB, logs WebSocket, log level. +"""System routes: external URL, shutdown action, ADB, logs WebSocket, log level. Extracted from system.py to keep files under 800 lines. """ @@ -17,13 +17,10 @@ from ledgrab.api.schemas.system import ( ExternalUrlResponse, LogLevelRequest, LogLevelResponse, - MQTTSettingsRequest, - MQTTSettingsResponse, ShutdownAction, ShutdownActionRequest, ShutdownActionResponse, ) -from ledgrab.config import get_config from ledgrab.storage.database import Database from ledgrab.utils import get_logger @@ -32,85 +29,6 @@ logger = get_logger(__name__) router = APIRouter() -# --------------------------------------------------------------------------- -# MQTT settings -# --------------------------------------------------------------------------- - - -def _load_mqtt_settings(db: Database) -> dict: - """Load MQTT settings: YAML config defaults overridden by DB settings.""" - cfg = get_config() - defaults = { - "enabled": cfg.mqtt.enabled, - "broker_host": cfg.mqtt.broker_host, - "broker_port": cfg.mqtt.broker_port, - "username": cfg.mqtt.username, - "password": cfg.mqtt.password, - "client_id": cfg.mqtt.client_id, - "base_topic": cfg.mqtt.base_topic, - } - overrides = db.get_setting("mqtt") - if overrides: - defaults.update(overrides) - return defaults - - -@router.get( - "/api/v1/system/mqtt/settings", - response_model=MQTTSettingsResponse, - tags=["System"], -) -async def get_mqtt_settings(_: AuthRequired, db: Database = Depends(get_database)): - """Get current MQTT broker settings. Password is masked.""" - s = _load_mqtt_settings(db) - return MQTTSettingsResponse( - enabled=s["enabled"], - broker_host=s["broker_host"], - broker_port=s["broker_port"], - username=s["username"], - password_set=bool(s.get("password")), - client_id=s["client_id"], - base_topic=s["base_topic"], - ) - - -@router.put( - "/api/v1/system/mqtt/settings", - response_model=MQTTSettingsResponse, - tags=["System"], -) -async def update_mqtt_settings( - _: AuthRequired, body: MQTTSettingsRequest, db: Database = Depends(get_database) -): - """Update MQTT broker settings. If password is empty string, the existing password is preserved.""" - current = _load_mqtt_settings(db) - - # If caller sends an empty password, keep the existing one - password = body.password if body.password else current.get("password", "") - - new_settings = { - "enabled": body.enabled, - "broker_host": body.broker_host, - "broker_port": body.broker_port, - "username": body.username, - "password": password, - "client_id": body.client_id, - "base_topic": body.base_topic, - } - db.set_setting("mqtt", new_settings) - logger.info("MQTT settings updated") - - return MQTTSettingsResponse( - enabled=new_settings["enabled"], - broker_host=new_settings["broker_host"], - broker_port=new_settings["broker_port"], - username=new_settings["username"], - password_set=bool(new_settings["password"]), - client_id=new_settings["client_id"], - base_topic=new_settings["base_topic"], - ) - - # --------------------------------------------------------------------------- # External URL setting # --------------------------------------------------------------------------- diff --git a/server/src/ledgrab/api/schemas/system.py b/server/src/ledgrab/api/schemas/system.py index de24d9b..9ac1c87 100644 --- a/server/src/ledgrab/api/schemas/system.py +++ b/server/src/ledgrab/api/schemas/system.py @@ -194,35 +194,6 @@ class BackupListResponse(BaseModel): count: int -# ─── MQTT schemas ────────────────────────────────────────────── - - -class MQTTSettingsResponse(BaseModel): - """MQTT broker settings response (password is masked).""" - - enabled: bool = Field(description="Whether MQTT is enabled") - broker_host: str = Field(description="MQTT broker hostname or IP") - broker_port: int = Field(ge=1, le=65535, description="MQTT broker port") - username: str = Field(description="MQTT username (empty = anonymous)") - password_set: bool = Field(description="Whether a password is configured") - client_id: str = Field(description="MQTT client ID") - base_topic: str = Field(description="Base topic prefix") - - -class MQTTSettingsRequest(BaseModel): - """MQTT broker settings update request.""" - - enabled: bool = Field(description="Whether MQTT is enabled") - broker_host: str = Field(description="MQTT broker hostname or IP") - broker_port: int = Field(ge=1, le=65535, description="MQTT broker port") - username: str = Field(default="", description="MQTT username (empty = anonymous)") - password: str = Field( - default="", description="MQTT password (empty = keep existing if omitted)" - ) - client_id: str = Field(default="ledgrab", description="MQTT client ID") - base_topic: str = Field(default="ledgrab", description="Base topic prefix") - - # ─── External URL schema ─────────────────────────────────────── From 02e2ea37f3524f442d3dc66b0648b28c5657ad45 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 4 Jun 2026 20:46:26 +0300 Subject: [PATCH 3/3] fix(scenes): sync brightness value-source change to live processor apply_scene_state computed brightness_changed = "brightness" in changed, but the change dict only ever uses the key "brightness_value_source_id", so the branch was dead and a running target's brightness source was never live-synced on scene activation (it only took effect after a restart). Check the correct key. --- server/src/ledgrab/core/scenes/scene_activator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/ledgrab/core/scenes/scene_activator.py b/server/src/ledgrab/core/scenes/scene_activator.py index e7eca89..0ec8dfc 100644 --- a/server/src/ledgrab/core/scenes/scene_activator.py +++ b/server/src/ledgrab/core/scenes/scene_activator.py @@ -93,7 +93,7 @@ async def apply_scene_state( proc = processor_manager.get_processor(ts.target_id) if proc and proc.is_running: css_changed = "color_strip_source_id" in changed - brightness_changed = "brightness" in changed + brightness_changed = "brightness_value_source_id" in changed settings_changed = "fps" in changed if css_changed: target.sync_with_manager(