- 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]>
72 lines
1.9 KiB
Python
72 lines
1.9 KiB
Python
"""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()
|