mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-09 18:33:14 +02:00
feat: implement setup status check and reject subsequent setups after completion
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user