feat: optional auth + backup/restore reliability fixes
Some checks failed
Lint & Test / test (push) Failing after 29s

Auth is now optional: when `auth.api_keys` is empty, all endpoints are
open (no login screen, no Bearer tokens). Health endpoint reports
`auth_required` so the frontend knows which mode to use.

Backup/restore fixes:
- Auto-backup uses atomic writes (was `write_text`, risked corruption)
- Startup backup skipped if recent backup exists (<5 min cooldown),
  preventing rapid restarts from rotating out good backups
- Restore rejects all-empty backups to prevent accidental data wipes
- Store saves frozen after restore to prevent stale in-memory data
  from overwriting freshly-restored files before restart completes
- Missing stores during restore logged as warnings
- STORE_MAP completeness verified at startup against StorageConfig
This commit is contained in:
2026-03-23 14:50:25 +03:00
parent cd3137b0ec
commit 4975a74ff3
18 changed files with 189 additions and 67 deletions

View File

@@ -21,7 +21,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_SERVER_NAME, default="LED Screen Controller"): str,
vol.Required(CONF_SERVER_URL, default="http://localhost:8080"): str,
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_API_KEY, default=""): str,
}
)
@@ -57,21 +57,25 @@ async def validate_server(
except aiohttp.ClientError as err:
raise ConnectionError(f"Cannot connect to server: {err}") from err
# Step 2: Validate API key via authenticated endpoint
headers = {"Authorization": f"Bearer {api_key}"}
try:
async with session.get(
f"{server_url}/api/v1/output-targets",
headers=headers,
timeout=timeout,
) as resp:
if resp.status == 401:
raise PermissionError("Invalid API key")
resp.raise_for_status()
except PermissionError:
raise
except aiohttp.ClientError as err:
raise ConnectionError(f"API request failed: {err}") from err
# Step 2: Validate API key via authenticated endpoint (skip if no key and auth not required)
auth_required = data.get("auth_required", True)
if api_key:
headers = {"Authorization": f"Bearer {api_key}"}
try:
async with session.get(
f"{server_url}/api/v1/output-targets",
headers=headers,
timeout=timeout,
) as resp:
if resp.status == 401:
raise PermissionError("Invalid API key")
resp.raise_for_status()
except PermissionError:
raise
except aiohttp.ClientError as err:
raise ConnectionError(f"API request failed: {err}") from err
elif auth_required:
raise PermissionError("Server requires an API key")
return {"version": version}