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
This commit is contained in:
Claude
2025-12-05 16:50:21 +00:00
parent a5d8d586e1
commit 796e303665
9 changed files with 11 additions and 36 deletions
@@ -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")
@@ -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"
-3
View File
@@ -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")
+7 -6
View File
@@ -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(
+4 -4
View File
@@ -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;
-1
View File
@@ -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):
-1
View File
@@ -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):
-1
View File
@@ -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):
-1
View File
@@ -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):