fix: authenticate device config sync like upload client
- send X-Device-Name and X-Api-Key to /api/device-config - require YARIS_DEVICE_NAME alongside shared upload creds - fix server create_device output to match actual Pi env names - document shared upload/config sync credential requirements Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -1505,7 +1505,8 @@ phase roadmap.
|
|||||||
- Detects changes via config_version
|
- Detects changes via config_version
|
||||||
- Stores synced config in SQLite
|
- Stores synced config in SQLite
|
||||||
- Reuses the existing upload credentials from `db/upload_creds.env`
|
- Reuses the existing upload credentials from `db/upload_creds.env`
|
||||||
(`YARIS_SERVER_URL` + `YARIS_API_KEY`; `YARIS_DEVICE_KEY` also accepted as an override)
|
(`YARIS_SERVER_URL` + `YARIS_DEVICE_NAME` + `YARIS_API_KEY`;
|
||||||
|
`YARIS_DEVICE_KEY` also accepted as an override for the key)
|
||||||
- Ready for hot-reload on config change (future: signal handler)
|
- Ready for hot-reload on config change (future: signal handler)
|
||||||
|
|
||||||
- **Pi startup** (`app.py`):
|
- **Pi startup** (`app.py`):
|
||||||
@@ -1526,7 +1527,8 @@ phase roadmap.
|
|||||||
#### Testing
|
#### Testing
|
||||||
- Server endpoints accessible via `/api/device-config`
|
- Server endpoints accessible via `/api/device-config`
|
||||||
- Pi thread starts on boot (check logs for connectivity attempts)
|
- Pi thread starts on boot (check logs for connectivity attempts)
|
||||||
- Sync disabled if `YARIS_SERVER_URL` or an API key (`YARIS_API_KEY` / `YARIS_DEVICE_KEY`) is not set
|
- Sync disabled if `YARIS_SERVER_URL`, `YARIS_DEVICE_NAME`, or an API key
|
||||||
|
(`YARIS_API_KEY` / `YARIS_DEVICE_KEY`) is not set
|
||||||
- **2026-08-10 access verification**: SSH access confirmed to Pi at `[email protected]`
|
- **2026-08-10 access verification**: SSH access confirmed to Pi at `[email protected]`
|
||||||
(new WiFi card/subnet) and to unraid via `ssh unraid` (`[email protected]`).
|
(new WiFi card/subnet) and to unraid via `ssh unraid` (`[email protected]`).
|
||||||
Follow-up deployment succeeded the same day: Pi fast-forwarded from `d7b9bb6` to `cd004fb`,
|
Follow-up deployment succeeded the same day: Pi fast-forwarded from `d7b9bb6` to `cd004fb`,
|
||||||
|
|||||||
+5
-4
@@ -204,10 +204,11 @@ nano /home/yaris/app/yaris-obd/config/device.json
|
|||||||
- **`avatar_path`**: Avatar image shown in topbar; must exist in `static/img/` (replace `static/img/yaris.png` with your own image, or point to a different file)
|
- **`avatar_path`**: Avatar image shown in topbar; must exist in `static/img/` (replace `static/img/yaris.png` with your own image, or point to a different file)
|
||||||
|
|
||||||
If you want the remote server to become the source of truth instead, keep the Pi
|
If you want the remote server to become the source of truth instead, keep the Pi
|
||||||
upload credentials in `db/upload_creds.env` (`YARIS_SERVER_URL` + `YARIS_API_KEY`)
|
upload credentials in `db/upload_creds.env` (`YARIS_SERVER_URL`,
|
||||||
and enable **Sync with server** in the local Settings page. The same creds file is
|
`YARIS_DEVICE_NAME`, and `YARIS_API_KEY`) and enable **Sync with server** in the
|
||||||
reused for config sync; when the server pushes new identity values, the running Pi
|
local Settings page. The same creds file is reused for config sync; when the
|
||||||
UI hot-reloads them on the next successful sync.
|
server pushes new identity values, the running Pi UI hot-reloads them on the next
|
||||||
|
successful sync.
|
||||||
|
|
||||||
After editing, restart the app:
|
After editing, restart the app:
|
||||||
|
|
||||||
|
|||||||
+14
-21
@@ -40,8 +40,9 @@ def _has_internet():
|
|||||||
|
|
||||||
def _server_credentials():
|
def _server_credentials():
|
||||||
server_url = os.getenv("YARIS_SERVER_URL", "").rstrip("/")
|
server_url = os.getenv("YARIS_SERVER_URL", "").rstrip("/")
|
||||||
|
device_name = os.getenv("YARIS_DEVICE_NAME") or os.getenv("YARIS_UPLOAD_DEVICE_NAME", "")
|
||||||
device_key = os.getenv("YARIS_DEVICE_KEY") or os.getenv("YARIS_API_KEY", "")
|
device_key = os.getenv("YARIS_DEVICE_KEY") or os.getenv("YARIS_API_KEY", "")
|
||||||
return server_url, device_key
|
return server_url, device_name, device_key
|
||||||
|
|
||||||
|
|
||||||
def start_sync():
|
def start_sync():
|
||||||
@@ -71,47 +72,39 @@ def _sync_loop():
|
|||||||
"""Periodically sync device config from server when internet available."""
|
"""Periodically sync device config from server when internet available."""
|
||||||
global _SYNC_CONFIG_VERSION
|
global _SYNC_CONFIG_VERSION
|
||||||
|
|
||||||
server_url, device_key = _server_credentials()
|
server_url, device_name, device_key = _server_credentials()
|
||||||
|
|
||||||
if not server_url or not device_key:
|
if not server_url or not device_name or not device_key:
|
||||||
return # Sync disabled if server URL or device key not configured
|
return
|
||||||
|
|
||||||
# Sync every 5 minutes (300 sec) when connected, backoff to 60 sec when offline
|
|
||||||
sync_interval = 300
|
sync_interval = 300
|
||||||
offline_interval = 60
|
offline_interval = 60
|
||||||
|
|
||||||
while not _SYNC_STOP:
|
while not _SYNC_STOP:
|
||||||
try:
|
try:
|
||||||
is_connected = _has_internet()
|
if _has_internet():
|
||||||
|
|
||||||
if is_connected:
|
|
||||||
# Fetch device config from server
|
|
||||||
headers = {"Authorization": f"Bearer {device_key}"}
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{server_url.rstrip('/')}/api/device-config",
|
f"{server_url}/api/device-config",
|
||||||
headers=headers,
|
headers={
|
||||||
timeout=5
|
"X-Device-Name": device_name,
|
||||||
|
"X-Api-Key": device_key,
|
||||||
|
},
|
||||||
|
timeout=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
config = response.json()
|
config = response.json()
|
||||||
|
|
||||||
# Check if config changed
|
|
||||||
new_version = config.get("config_version", 0)
|
new_version = config.get("config_version", 0)
|
||||||
if new_version != _SYNC_CONFIG_VERSION:
|
if new_version != _SYNC_CONFIG_VERSION:
|
||||||
_SYNC_CONFIG_VERSION = new_version
|
_SYNC_CONFIG_VERSION = new_version
|
||||||
# Store in database
|
|
||||||
_store_synced_config(config)
|
_store_synced_config(config)
|
||||||
# Signal that config changed (for hot-reload)
|
|
||||||
_on_config_changed()
|
_on_config_changed()
|
||||||
|
|
||||||
time.sleep(sync_interval)
|
time.sleep(sync_interval)
|
||||||
else:
|
else:
|
||||||
# Offline: backoff to less frequent checks
|
|
||||||
time.sleep(offline_interval)
|
time.sleep(offline_interval)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log error but don't crash sync thread
|
|
||||||
print(f"Device config sync error: {e}")
|
print(f"Device config sync error: {e}")
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
|
|
||||||
@@ -134,8 +127,8 @@ def _on_config_changed():
|
|||||||
|
|
||||||
def is_sync_enabled():
|
def is_sync_enabled():
|
||||||
"""Check if device config sync from server is enabled."""
|
"""Check if device config sync from server is enabled."""
|
||||||
server_url, device_key = _server_credentials()
|
server_url, device_name, device_key = _server_credentials()
|
||||||
return bool(server_url) and bool(device_key)
|
return bool(server_url) and bool(device_name) and bool(device_key)
|
||||||
|
|
||||||
|
|
||||||
def get_synced_config_version():
|
def get_synced_config_version():
|
||||||
|
|||||||
+2
-2
@@ -97,8 +97,8 @@ Run this once per device (e.g. once for the Pi) after the stack is up:
|
|||||||
docker compose exec api python3 scripts/create_device.py yaris-2013
|
docker compose exec api python3 scripts/create_device.py yaris-2013
|
||||||
```
|
```
|
||||||
|
|
||||||
This prints a one-time API key — copy the `YARIS_UPLOAD_DEVICE_NAME` /
|
This prints a one-time API key — copy the `YARIS_DEVICE_NAME` /
|
||||||
`YARIS_UPLOAD_API_KEY` values into the Pi's upload-client config. Only the
|
`YARIS_API_KEY` values into the Pi's upload-client config. Only the
|
||||||
key's sha256 hash is stored server-side; the raw key is never shown again
|
key's sha256 hash is stored server-side; the raw key is never shown again
|
||||||
(re-run the script with the same device name to rotate/replace the key).
|
(re-run the script with the same device name to rotate/replace the key).
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ def main():
|
|||||||
|
|
||||||
print(f"Device '{device_name}' registered (id={device_id}).")
|
print(f"Device '{device_name}' registered (id={device_id}).")
|
||||||
print()
|
print()
|
||||||
print("Save these on the Pi (e.g. in server/.env-style local config, gitignored):")
|
print("Save these on the Pi (e.g. in db/upload_creds.env, gitignored):")
|
||||||
print(f" YARIS_UPLOAD_DEVICE_NAME={device_name}")
|
print(f" YARIS_DEVICE_NAME={device_name}")
|
||||||
print(f" YARIS_UPLOAD_API_KEY={raw_key}")
|
print(f" YARIS_API_KEY={raw_key}")
|
||||||
print()
|
print()
|
||||||
print("This key will not be shown again - only its hash is stored server-side.")
|
print("This key will not be shown again - only its hash is stored server-side.")
|
||||||
|
|
||||||
|
|||||||
@@ -56,11 +56,20 @@ class DeviceSyncReloadCallbackTest(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
app_device_sync._RELOAD_CALLBACK = original
|
app_device_sync._RELOAD_CALLBACK = original
|
||||||
|
|
||||||
@patch.dict(os.environ, {"YARIS_SERVER_URL": "https://example.test", "YARIS_API_KEY": "upload-key"}, clear=True)
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"YARIS_SERVER_URL": "https://example.test",
|
||||||
|
"YARIS_DEVICE_NAME": "yaris-2013",
|
||||||
|
"YARIS_API_KEY": "upload-key",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
def test_sync_credentials_fall_back_to_upload_api_key(self):
|
def test_sync_credentials_fall_back_to_upload_api_key(self):
|
||||||
server_url, device_key = app_device_sync._server_credentials()
|
server_url, device_name, device_key = app_device_sync._server_credentials()
|
||||||
|
|
||||||
self.assertEqual(server_url, "https://example.test")
|
self.assertEqual(server_url, "https://example.test")
|
||||||
|
self.assertEqual(device_name, "yaris-2013")
|
||||||
self.assertEqual(device_key, "upload-key")
|
self.assertEqual(device_key, "upload-key")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user