From 796e303665ddc6cb233bc3762059ff47511e07cd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Dec 2025 16:50:21 +0000 Subject: [PATCH 1/2] Remove internal UUID fields from API responses Internal database UUIDs (id, node_id, receiver_node_id) were being exposed in API responses. These are implementation details that should not be visible to API consumers. The canonical identifier for nodes is the 64-char hex public_key. Changes: - Remove id, node_id from NodeTagRead, NodeRead schemas - Remove id from MemberRead schema - Remove id, receiver_node_id, node_id from MessageRead, AdvertisementRead, TracePathRead, TelemetryRead schemas - Update web map component to use public_key instead of member.id for owner filtering - Update tests to not assert on removed fields --- src/meshcore_hub/common/schemas/members.py | 1 - src/meshcore_hub/common/schemas/messages.py | 18 ------------------ src/meshcore_hub/common/schemas/nodes.py | 3 --- src/meshcore_hub/web/routes/map.py | 13 +++++++------ src/meshcore_hub/web/templates/map.html | 8 ++++---- tests/test_api/test_advertisements.py | 1 - tests/test_api/test_messages.py | 1 - tests/test_api/test_telemetry.py | 1 - tests/test_api/test_trace_paths.py | 1 - 9 files changed, 11 insertions(+), 36 deletions(-) diff --git a/src/meshcore_hub/common/schemas/members.py b/src/meshcore_hub/common/schemas/members.py index ce34be8..f0c8702 100644 --- a/src/meshcore_hub/common/schemas/members.py +++ b/src/meshcore_hub/common/schemas/members.py @@ -83,7 +83,6 @@ class MemberUpdate(BaseModel): class MemberRead(BaseModel): """Schema for reading a member.""" - id: str = Field(..., description="Member UUID") name: str = Field(..., description="Member's display name") callsign: Optional[str] = Field(default=None, description="Amateur radio callsign") role: Optional[str] = Field(default=None, description="Member's role") diff --git a/src/meshcore_hub/common/schemas/messages.py b/src/meshcore_hub/common/schemas/messages.py index f17ee31..87246ae 100644 --- a/src/meshcore_hub/common/schemas/messages.py +++ b/src/meshcore_hub/common/schemas/messages.py @@ -9,10 +9,6 @@ from pydantic import BaseModel, Field class MessageRead(BaseModel): """Schema for reading a message.""" - id: str = Field(..., description="Message UUID") - receiver_node_id: Optional[str] = Field( - default=None, description="Receiving interface node UUID" - ) receiver_public_key: Optional[str] = Field( default=None, description="Receiving interface node public key" ) @@ -85,14 +81,9 @@ class MessageFilters(BaseModel): class AdvertisementRead(BaseModel): """Schema for reading an advertisement.""" - id: str = Field(..., description="Advertisement UUID") - receiver_node_id: Optional[str] = Field( - default=None, description="Receiving interface node UUID" - ) receiver_public_key: Optional[str] = Field( default=None, description="Receiving interface node public key" ) - node_id: Optional[str] = Field(default=None, description="Advertised node UUID") public_key: str = Field(..., description="Advertised public key") name: Optional[str] = Field(default=None, description="Advertised name") adv_type: Optional[str] = Field(default=None, description="Node type") @@ -116,10 +107,6 @@ class AdvertisementList(BaseModel): class TracePathRead(BaseModel): """Schema for reading a trace path.""" - id: str = Field(..., description="Trace path UUID") - receiver_node_id: Optional[str] = Field( - default=None, description="Receiving interface node UUID" - ) receiver_public_key: Optional[str] = Field( default=None, description="Receiving interface node public key" ) @@ -153,14 +140,9 @@ class TracePathList(BaseModel): class TelemetryRead(BaseModel): """Schema for reading a telemetry record.""" - id: str = Field(..., description="Telemetry UUID") - receiver_node_id: Optional[str] = Field( - default=None, description="Receiving interface node UUID" - ) receiver_public_key: Optional[str] = Field( default=None, description="Receiving interface node public key" ) - node_id: Optional[str] = Field(default=None, description="Reporting node UUID") node_public_key: str = Field(..., description="Reporting node public key") parsed_data: Optional[dict] = Field( default=None, description="Decoded sensor readings" diff --git a/src/meshcore_hub/common/schemas/nodes.py b/src/meshcore_hub/common/schemas/nodes.py index e4ebdf9..7844083 100644 --- a/src/meshcore_hub/common/schemas/nodes.py +++ b/src/meshcore_hub/common/schemas/nodes.py @@ -41,8 +41,6 @@ class NodeTagUpdate(BaseModel): class NodeTagRead(BaseModel): """Schema for reading a node tag.""" - id: str = Field(..., description="Tag UUID") - node_id: str = Field(..., description="Parent node UUID") key: str = Field(..., description="Tag name/key") value: Optional[str] = Field(default=None, description="Tag value") value_type: str = Field(..., description="Value type hint") @@ -56,7 +54,6 @@ class NodeTagRead(BaseModel): class NodeRead(BaseModel): """Schema for reading a node.""" - id: str = Field(..., description="Node UUID") public_key: str = Field(..., description="Node's 64-character hex public key") name: Optional[str] = Field(default=None, description="Node display name") adv_type: Optional[str] = Field(default=None, description="Advertisement type") diff --git a/src/meshcore_hub/web/routes/map.py b/src/meshcore_hub/web/routes/map.py index d0cd8b7..1c7238c 100644 --- a/src/meshcore_hub/web/routes/map.py +++ b/src/meshcore_hub/web/routes/map.py @@ -43,13 +43,14 @@ async def map_data(request: Request) -> JSONResponse: if members_response.status_code == 200: members_data = members_response.json() for member in members_data.get("items", []): - member_info = { - "id": member.get("id"), - "name": member.get("name"), - "callsign": member.get("callsign"), - } - members_list.append(member_info) + # Only include members with public_key (required for node ownership) if member.get("public_key"): + member_info = { + "public_key": member.get("public_key"), + "name": member.get("name"), + "callsign": member.get("callsign"), + } + members_list.append(member_info) members_by_key[member["public_key"]] = member_info else: logger.warning( diff --git a/src/meshcore_hub/web/templates/map.html b/src/meshcore_hub/web/templates/map.html index 25943bc..fee1d3b 100644 --- a/src/meshcore_hub/web/templates/map.html +++ b/src/meshcore_hub/web/templates/map.html @@ -211,7 +211,7 @@ // Owner filter if (ownerFilter) { - if (!node.owner || node.owner.id !== ownerFilter) return false; + if (!node.owner || node.owner.public_key !== ownerFilter) return false; } return true; @@ -269,12 +269,12 @@ const ownersWithNodes = new Set(); allNodes.forEach(node => { if (node.owner) { - ownersWithNodes.add(node.owner.id); + ownersWithNodes.add(node.owner.public_key); } }); // Filter members to only those who own nodes on the map - const relevantMembers = allMembers.filter(m => ownersWithNodes.has(m.id)); + const relevantMembers = allMembers.filter(m => ownersWithNodes.has(m.public_key)); // Sort by name relevantMembers.sort((a, b) => a.name.localeCompare(b.name)); @@ -282,7 +282,7 @@ // Add options relevantMembers.forEach(member => { const option = document.createElement('option'); - option.value = member.id; + option.value = member.public_key; option.textContent = member.callsign ? `${member.name} (${member.callsign})` : member.name; diff --git a/tests/test_api/test_advertisements.py b/tests/test_api/test_advertisements.py index 6e2763c..89aaca2 100644 --- a/tests/test_api/test_advertisements.py +++ b/tests/test_api/test_advertisements.py @@ -49,7 +49,6 @@ class TestGetAdvertisement: ) assert response.status_code == 200 data = response.json() - assert data["id"] == sample_advertisement.id assert data["public_key"] == sample_advertisement.public_key def test_get_advertisement_not_found(self, client_no_auth): diff --git a/tests/test_api/test_messages.py b/tests/test_api/test_messages.py index f7b1425..8924d01 100644 --- a/tests/test_api/test_messages.py +++ b/tests/test_api/test_messages.py @@ -51,7 +51,6 @@ class TestGetMessage: response = client_no_auth.get(f"/api/v1/messages/{sample_message.id}") assert response.status_code == 200 data = response.json() - assert data["id"] == sample_message.id assert data["text"] == sample_message.text def test_get_message_not_found(self, client_no_auth): diff --git a/tests/test_api/test_telemetry.py b/tests/test_api/test_telemetry.py index ff7c802..73da843 100644 --- a/tests/test_api/test_telemetry.py +++ b/tests/test_api/test_telemetry.py @@ -45,7 +45,6 @@ class TestGetTelemetry: response = client_no_auth.get(f"/api/v1/telemetry/{sample_telemetry.id}") assert response.status_code == 200 data = response.json() - assert data["id"] == sample_telemetry.id assert data["node_public_key"] == sample_telemetry.node_public_key def test_get_telemetry_not_found(self, client_no_auth): diff --git a/tests/test_api/test_trace_paths.py b/tests/test_api/test_trace_paths.py index 0bdde36..a58e71e 100644 --- a/tests/test_api/test_trace_paths.py +++ b/tests/test_api/test_trace_paths.py @@ -31,7 +31,6 @@ class TestGetTracePath: response = client_no_auth.get(f"/api/v1/trace-paths/{sample_trace_path.id}") assert response.status_code == 200 data = response.json() - assert data["id"] == sample_trace_path.id assert data["path_hashes"] == sample_trace_path.path_hashes def test_get_trace_path_not_found(self, client_no_auth): From 84e83a33849ee167c2a99c548ec74e694f87c68f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Dec 2025 16:57:24 +0000 Subject: [PATCH 2/2] Rename receiver_public_key to received_by Shorter, cleaner field name for the receiving interface node's public key in API responses. --- src/meshcore_hub/api/routes/advertisements.py | 12 ++++++------ src/meshcore_hub/api/routes/messages.py | 12 ++++++------ src/meshcore_hub/api/routes/telemetry.py | 12 ++++++------ src/meshcore_hub/api/routes/trace_paths.py | 12 ++++++------ src/meshcore_hub/common/schemas/messages.py | 8 ++++---- src/meshcore_hub/web/templates/messages.html | 4 ++-- src/meshcore_hub/web/templates/node_detail.html | 8 ++++---- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/meshcore_hub/api/routes/advertisements.py b/src/meshcore_hub/api/routes/advertisements.py index e70f3bf..44fb44e 100644 --- a/src/meshcore_hub/api/routes/advertisements.py +++ b/src/meshcore_hub/api/routes/advertisements.py @@ -20,7 +20,7 @@ async def list_advertisements( _: RequireRead, session: DbSession, public_key: Optional[str] = Query(None, description="Filter by public key"), - receiver_public_key: Optional[str] = Query( + received_by: Optional[str] = Query( None, description="Filter by receiver node public key" ), since: Optional[datetime] = Query(None, description="Start timestamp"), @@ -40,8 +40,8 @@ async def list_advertisements( if public_key: query = query.where(Advertisement.public_key == public_key) - if receiver_public_key: - query = query.where(ReceiverNode.public_key == receiver_public_key) + if received_by: + query = query.where(ReceiverNode.public_key == received_by) if since: query = query.where(Advertisement.received_at >= since) @@ -59,13 +59,13 @@ async def list_advertisements( # Execute results = session.execute(query).all() - # Build response with receiver_public_key + # Build response with received_by items = [] for adv, receiver_pk in results: data = { "id": adv.id, "receiver_node_id": adv.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "node_id": adv.node_id, "public_key": adv.public_key, "name": adv.name, @@ -106,7 +106,7 @@ async def get_advertisement( data = { "id": adv.id, "receiver_node_id": adv.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "node_id": adv.node_id, "public_key": adv.public_key, "name": adv.name, diff --git a/src/meshcore_hub/api/routes/messages.py b/src/meshcore_hub/api/routes/messages.py index 1e5e981..98bfc04 100644 --- a/src/meshcore_hub/api/routes/messages.py +++ b/src/meshcore_hub/api/routes/messages.py @@ -22,7 +22,7 @@ async def list_messages( message_type: Optional[str] = Query(None, description="Filter by message type"), pubkey_prefix: Optional[str] = Query(None, description="Filter by sender prefix"), channel_idx: Optional[int] = Query(None, description="Filter by channel"), - receiver_public_key: Optional[str] = Query( + received_by: Optional[str] = Query( None, description="Filter by receiver node public key" ), since: Optional[datetime] = Query(None, description="Start timestamp"), @@ -49,8 +49,8 @@ async def list_messages( if channel_idx is not None: query = query.where(Message.channel_idx == channel_idx) - if receiver_public_key: - query = query.where(ReceiverNode.public_key == receiver_public_key) + if received_by: + query = query.where(ReceiverNode.public_key == received_by) if since: query = query.where(Message.received_at >= since) @@ -96,13 +96,13 @@ async def list_messages( for public_key, value in session.execute(friendly_name_query).all(): friendly_names[public_key[:12]] = value - # Build response with sender info and receiver_public_key + # Build response with sender info and received_by items = [] for m, receiver_pk in results: msg_dict = { "id": m.id, "receiver_node_id": m.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "message_type": m.message_type, "pubkey_prefix": m.pubkey_prefix, "sender_name": ( @@ -153,7 +153,7 @@ async def get_message( data = { "id": message.id, "receiver_node_id": message.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "message_type": message.message_type, "pubkey_prefix": message.pubkey_prefix, "channel_idx": message.channel_idx, diff --git a/src/meshcore_hub/api/routes/telemetry.py b/src/meshcore_hub/api/routes/telemetry.py index 3aba139..adab7db 100644 --- a/src/meshcore_hub/api/routes/telemetry.py +++ b/src/meshcore_hub/api/routes/telemetry.py @@ -20,7 +20,7 @@ async def list_telemetry( _: RequireRead, session: DbSession, node_public_key: Optional[str] = Query(None, description="Filter by node"), - receiver_public_key: Optional[str] = Query( + received_by: Optional[str] = Query( None, description="Filter by receiver node public key" ), since: Optional[datetime] = Query(None, description="Start timestamp"), @@ -40,8 +40,8 @@ async def list_telemetry( if node_public_key: query = query.where(Telemetry.node_public_key == node_public_key) - if receiver_public_key: - query = query.where(ReceiverNode.public_key == receiver_public_key) + if received_by: + query = query.where(ReceiverNode.public_key == received_by) if since: query = query.where(Telemetry.received_at >= since) @@ -59,13 +59,13 @@ async def list_telemetry( # Execute results = session.execute(query).all() - # Build response with receiver_public_key + # Build response with received_by items = [] for tel, receiver_pk in results: data = { "id": tel.id, "receiver_node_id": tel.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "node_id": tel.node_id, "node_public_key": tel.node_public_key, "parsed_data": tel.parsed_data, @@ -104,7 +104,7 @@ async def get_telemetry( data = { "id": tel.id, "receiver_node_id": tel.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "node_id": tel.node_id, "node_public_key": tel.node_public_key, "parsed_data": tel.parsed_data, diff --git a/src/meshcore_hub/api/routes/trace_paths.py b/src/meshcore_hub/api/routes/trace_paths.py index 5cdba46..e72569e 100644 --- a/src/meshcore_hub/api/routes/trace_paths.py +++ b/src/meshcore_hub/api/routes/trace_paths.py @@ -19,7 +19,7 @@ router = APIRouter() async def list_trace_paths( _: RequireRead, session: DbSession, - receiver_public_key: Optional[str] = Query( + received_by: Optional[str] = Query( None, description="Filter by receiver node public key" ), since: Optional[datetime] = Query(None, description="Start timestamp"), @@ -36,8 +36,8 @@ async def list_trace_paths( ReceiverNode, TracePath.receiver_node_id == ReceiverNode.id ) - if receiver_public_key: - query = query.where(ReceiverNode.public_key == receiver_public_key) + if received_by: + query = query.where(ReceiverNode.public_key == received_by) if since: query = query.where(TracePath.received_at >= since) @@ -55,13 +55,13 @@ async def list_trace_paths( # Execute results = session.execute(query).all() - # Build response with receiver_public_key + # Build response with received_by items = [] for tp, receiver_pk in results: data = { "id": tp.id, "receiver_node_id": tp.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "initiator_tag": tp.initiator_tag, "path_len": tp.path_len, "flags": tp.flags, @@ -104,7 +104,7 @@ async def get_trace_path( data = { "id": tp.id, "receiver_node_id": tp.receiver_node_id, - "receiver_public_key": receiver_pk, + "received_by": receiver_pk, "initiator_tag": tp.initiator_tag, "path_len": tp.path_len, "flags": tp.flags, diff --git a/src/meshcore_hub/common/schemas/messages.py b/src/meshcore_hub/common/schemas/messages.py index 87246ae..b57a526 100644 --- a/src/meshcore_hub/common/schemas/messages.py +++ b/src/meshcore_hub/common/schemas/messages.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field class MessageRead(BaseModel): """Schema for reading a message.""" - receiver_public_key: Optional[str] = Field( + received_by: Optional[str] = Field( default=None, description="Receiving interface node public key" ) message_type: str = Field(..., description="Message type (contact, channel)") @@ -81,7 +81,7 @@ class MessageFilters(BaseModel): class AdvertisementRead(BaseModel): """Schema for reading an advertisement.""" - receiver_public_key: Optional[str] = Field( + received_by: Optional[str] = Field( default=None, description="Receiving interface node public key" ) public_key: str = Field(..., description="Advertised public key") @@ -107,7 +107,7 @@ class AdvertisementList(BaseModel): class TracePathRead(BaseModel): """Schema for reading a trace path.""" - receiver_public_key: Optional[str] = Field( + received_by: Optional[str] = Field( default=None, description="Receiving interface node public key" ) initiator_tag: int = Field(..., description="Trace identifier") @@ -140,7 +140,7 @@ class TracePathList(BaseModel): class TelemetryRead(BaseModel): """Schema for reading a telemetry record.""" - receiver_public_key: Optional[str] = Field( + received_by: Optional[str] = Field( default=None, description="Receiving interface node public key" ) node_public_key: str = Field(..., description="Reporting node public key") diff --git a/src/meshcore_hub/web/templates/messages.html b/src/meshcore_hub/web/templates/messages.html index c30b4c3..2d7471c 100644 --- a/src/meshcore_hub/web/templates/messages.html +++ b/src/meshcore_hub/web/templates/messages.html @@ -90,8 +90,8 @@ {{ msg.text or '-' }} - {% if msg.receiver_public_key %} - {{ msg.receiver_public_key[:8] }}... + {% if msg.received_by %} + {{ msg.received_by[:8] }}... {% else %} - {% endif %} diff --git a/src/meshcore_hub/web/templates/node_detail.html b/src/meshcore_hub/web/templates/node_detail.html index 59568fa..499692b 100644 --- a/src/meshcore_hub/web/templates/node_detail.html +++ b/src/meshcore_hub/web/templates/node_detail.html @@ -113,8 +113,8 @@ {{ adv.adv_type or '-' }} {{ adv.name or '-' }} - {% if adv.receiver_public_key %} - {{ adv.receiver_public_key[:8] }}... + {% if adv.received_by %} + {{ adv.received_by[:8] }}... {% else %} - {% endif %} @@ -156,8 +156,8 @@ {% endif %} - {% if tel.receiver_public_key %} - {{ tel.receiver_public_key[:8] }}... + {% if tel.received_by %} + {{ tel.received_by[:8] }}... {% else %} - {% endif %}