feat: add multi-device provisioning and local remote-config UI
- add multi-device portal device management, key rotation, and sync request APIs - add Pi settings UI for remote server URL, device name, and API key - add local test-connection and sync-now actions - make upload and config sync reuse dynamic shared credentials - add Phase 25.5c migration and docs updates Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""Multi-device provisioning helpers for the remote portal."""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import secrets
|
||||
|
||||
from . import db
|
||||
|
||||
|
||||
_DEVICE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$")
|
||||
|
||||
|
||||
def list_devices():
|
||||
return db.list_devices()
|
||||
|
||||
|
||||
def validate_device_name(name):
|
||||
value = (name or "").strip()
|
||||
if not value:
|
||||
return None, "Device name is required."
|
||||
if not _DEVICE_NAME_RE.fullmatch(value):
|
||||
return None, "Device name must be 1-100 chars using letters, numbers, dot, underscore, or dash."
|
||||
return value, None
|
||||
|
||||
|
||||
def create_device(name):
|
||||
value, error = validate_device_name(name)
|
||||
if error:
|
||||
return None, {"name": error}
|
||||
if db.get_device_by_name(value) is not None:
|
||||
return None, {"name": "A device with that name already exists."}
|
||||
|
||||
raw_key = secrets.token_urlsafe(32)
|
||||
device_id = db.create_device(value, _hash_key(raw_key))
|
||||
return {
|
||||
"device": db.get_device_by_id(device_id),
|
||||
"api_key": raw_key,
|
||||
}, {}
|
||||
|
||||
|
||||
def rotate_device_key(device_id):
|
||||
device = db.get_device_by_id(device_id)
|
||||
if device is None:
|
||||
return None, {"error": "Device not found."}
|
||||
|
||||
raw_key = secrets.token_urlsafe(32)
|
||||
db.rotate_device_api_key(device_id, _hash_key(raw_key))
|
||||
return {
|
||||
"device": db.get_device_by_id(device_id),
|
||||
"api_key": raw_key,
|
||||
}, {}
|
||||
|
||||
|
||||
def request_sync(device_id):
|
||||
config_version = db.request_device_sync(device_id)
|
||||
if config_version is None:
|
||||
return None
|
||||
device = db.get_device_by_id(device_id)
|
||||
return {
|
||||
"device": device,
|
||||
"message": "Sync requested. The Pi will pick it up on its next online contact, or immediately if Sync now is used locally.",
|
||||
"config_version": config_version,
|
||||
}
|
||||
|
||||
|
||||
def acknowledge_sync(device_id, config_version):
|
||||
db.acknowledge_device_sync(device_id, config_version)
|
||||
|
||||
|
||||
def _hash_key(raw_key):
|
||||
return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
|
||||
Reference in New Issue
Block a user