feat(users): store device_serials on user docs for direct user->device lookup
Adds a `device_serials: [string]` array to Firestore `users` docs so the
MQTT ACL (and get_user_devices) can answer "which boards may this user
reach?" without streaming the entire devices collection.
- The serial is the value used in MQTT topics vesper/{serial}/...: the
device doc's `serial_number` (flashed into NVS, used by the firmware as
its MQTT id), falling back to the legacy `device_id` for old docs.
Centralised in users.service.device_serial_of().
- assign_device / unassign_device now write the device's user_list and the
user's device_serials (ArrayUnion/ArrayRemove) in one atomic batch.
- The device Manage tab endpoints (POST/DELETE /api/devices/{id}/user-list)
also edit user_list, so they get the same batched sync - otherwise the
most common assignment path would silently leave device_serials stale.
- get_user_devices resolves devices via device_serials with chunked
Firestore "in" queries instead of a full collection scan. Requires the
backfill script (next commit) to be run for existing assignments.
- New mqtt/app_users.py: resolves users by the `uid` FIELD (not doc id -
create_user uses .add(), FlutterFlow uses uid as doc id) with a 60s
in-process TTL cache. Assign/unassign, update, block/unblock and delete
invalidate that uid's entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from devices.models import (
|
||||
ResetStatsRequest, ResetStatsResult,
|
||||
)
|
||||
from devices import service
|
||||
from users import service as users_service
|
||||
import database as mqtt_db
|
||||
from mqtt.models import DeviceAlertEntry, DeviceAlertsResponse
|
||||
from shared.firebase import get_db as get_firestore
|
||||
@@ -461,10 +462,15 @@ async def add_user_to_device(
|
||||
elif isinstance(entry, str):
|
||||
existing_ids.add(entry.split("/")[-1])
|
||||
|
||||
# user_list and the user's device_serials (MQTT ACL lookup) commit together.
|
||||
user_ref = fs.collection("users").document(body.user_id)
|
||||
batch = fs.batch()
|
||||
if body.user_id not in existing_ids:
|
||||
user_ref = fs.collection("users").document(body.user_id)
|
||||
user_list.append(user_ref)
|
||||
device_ref.update({"user_list": user_list})
|
||||
batch.update(device_ref, {"user_list": user_list})
|
||||
users_service.stage_device_serial_link(batch, user_ref, users_service.device_serial_of(data), linked=True)
|
||||
batch.commit()
|
||||
users_service.invalidate_mqtt_acl_cache(user_doc.to_dict())
|
||||
|
||||
await log_action(db, _user.sub, _user.name or _user.email, "UPDATE", "device",
|
||||
device_id, device_id, meta={"action_detail": "user_added",
|
||||
@@ -500,7 +506,15 @@ async def remove_user_from_device(
|
||||
|
||||
# Remove any entry that resolves to this user_id (handles both DocRef and string paths)
|
||||
new_list = [entry for entry in user_list if not resolves_to(entry, user_id)]
|
||||
device_ref.update({"user_list": new_list})
|
||||
batch = fs.batch()
|
||||
batch.update(device_ref, {"user_list": new_list})
|
||||
user_ref = fs.collection("users").document(user_id)
|
||||
user_doc = user_ref.get()
|
||||
if user_doc.exists:
|
||||
users_service.stage_device_serial_link(batch, user_ref, users_service.device_serial_of(data), linked=False)
|
||||
batch.commit()
|
||||
if user_doc.exists:
|
||||
users_service.invalidate_mqtt_acl_cache(user_doc.to_dict())
|
||||
|
||||
await log_action(db, _user.sub, _user.name or _user.email, "UPDATE", "device",
|
||||
device_id, device_id, meta={"action_detail": "user_removed",
|
||||
|
||||
Reference in New Issue
Block a user