- add portal API and UI action to delete non-default devices - protect the configured portal default device from deletion - document that trip/history pages still scope to the single DEVICE_NAME Co-authored-by: Copilot <[email protected]>
87 lines
2.4 KiB
Python
87 lines
2.4 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 delete_device(device_id, protected_device_name=None):
|
|
device = db.get_device_by_id(device_id)
|
|
if device is None:
|
|
return False, {"error": "Device not found."}
|
|
if protected_device_name and device["name"] == protected_device_name:
|
|
return False, {
|
|
"error": "Cannot delete the portal's default device while DEVICE_NAME still points to it.",
|
|
}
|
|
|
|
deleted_id = db.delete_device(device_id)
|
|
if deleted_id is None:
|
|
return False, {"error": "Device not found."}
|
|
return True, {}
|
|
|
|
|
|
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()
|