Files
yaris-obd/server/modules/device_registry.py
Mathieu MoranandCopilot 675bffb4a4 feat: add server-side device deletion
- 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]>
2026-08-10 18:09:15 -04:00

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()