feat: implement setup status check and reject subsequent setups after completion

This commit is contained in:
Lloyd
2026-05-21 11:32:08 +01:00
parent 8f3477ddd6
commit d25e97af3c
2 changed files with 74 additions and 21 deletions
+48 -21
View File
@@ -297,6 +297,25 @@ class APIEndpoints:
values = [v if v is not None else 0 for v in data_points]
return [[timestamps_ms[i], values[i]] for i in range(min(len(values), len(timestamps_ms)))]
def _setup_status_from_config(self, config: dict) -> tuple[bool, dict]:
"""Return whether first-run setup should still be available."""
node_name = config.get("repeater", {}).get("node_name", "")
has_default_name = node_name in ["mesh-repeater-01", ""]
admin_password = config.get("repeater", {}).get("security", {}).get("admin_password", "")
has_default_password = admin_password in ["admin123", ""]
radio_type_raw = config.get("radio_type")
radio_type = "" if radio_type_raw is None else str(radio_type_raw).lower().strip()
radio_not_configured = radio_type in ("", "none", "null", "disabled", "off", "no_radio")
reasons = {
"default_name": has_default_name,
"default_password": has_default_password,
"radio_not_configured": radio_not_configured,
}
return has_default_name or has_default_password or radio_not_configured, reasons
# ============================================================================
# SETUP WIZARD ENDPOINTS
# ============================================================================
@@ -306,30 +325,22 @@ class APIEndpoints:
def needs_setup(self):
"""Check if the repeater needs initial setup configuration"""
try:
# Prefer the on-disk config so this reflects current persisted state.
import yaml
config = self.config
try:
with open(self._config_path, "r") as f:
config = yaml.safe_load(f) or {}
except Exception:
# Fall back to in-memory config if file cannot be read.
pass
# Check for default values that indicate first-time setup
node_name = config.get("repeater", {}).get("node_name", "")
has_default_name = node_name in ["mesh-repeater-01", ""]
admin_password = (
config.get("repeater", {}).get("security", {}).get("admin_password", "")
)
has_default_password = admin_password in ["admin123", ""]
radio_type_raw = config.get("radio_type")
radio_type = "" if radio_type_raw is None else str(radio_type_raw).lower().strip()
radio_not_configured = radio_type in ("", "none", "null", "disabled", "off", "no_radio")
needs_setup = has_default_name or has_default_password or radio_not_configured
needs_setup, reasons = self._setup_status_from_config(config)
return {
"needs_setup": needs_setup,
"reasons": {
"default_name": has_default_name,
"default_password": has_default_password,
"radio_not_configured": radio_not_configured,
},
"reasons": reasons,
}
except Exception as e:
logger.error(f"Error checking setup status: {e}")
@@ -412,6 +423,24 @@ class APIEndpoints:
self._require_post()
data = cherrypy.request.json
import yaml
# Setup wizard is first-run only. After setup, use /auth/change_password
# and /api/update_radio_config for subsequent changes.
try:
with open(self._config_path, "r") as f:
current_config = yaml.safe_load(f) or {}
except Exception:
current_config = self.config or {}
needs_setup, _ = self._setup_status_from_config(current_config)
if not needs_setup:
cherrypy.response.status = 403
return {
"success": False,
"error": "Setup is already complete. Use authenticated endpoints for configuration changes.",
}
# Validate required fields
node_name = data.get("node_name", "").strip()
if not node_name:
@@ -452,8 +481,6 @@ class APIEndpoints:
else:
hw_config = {}
import yaml
# Read current config first so we can update it
with open(self._config_path, "r") as f:
config_yaml = yaml.safe_load(f)
+26
View File
@@ -206,3 +206,29 @@ def test_wizard_kiss_branch_unchanged(wizard_env, tmp_path):
assert written["radio_type"] == "kiss"
assert written["kiss"]["port"] == "/dev/ttyUSB0"
assert written["kiss"]["baud_rate"] == 115200
def test_wizard_rejected_after_setup_complete(wizard_env):
"""setup_wizard should be first-run only once config is already initialized."""
tmp_path, config_path, endpoints, set_request = wizard_env
configured = {
"repeater": {"node_name": "already-set", "security": {"admin_password": "verysecret"}},
"radio_type": "pymc_tcp",
"radio": {
"frequency": 869618000,
"spreading_factor": 8,
"bandwidth": 62500,
"coding_rate": 8,
},
}
with open(config_path, "w") as f:
yaml.safe_dump(configured, f)
body = dict(_BASE_REQUEST, hardware_key="pymc_tcp", pymc_tcp_host="modem.local")
set_request(body)
result = endpoints.setup_wizard()
assert result["success"] is False
assert "already complete" in result["error"].lower()