Split up community broker fields and reformat MQTT config page

This commit is contained in:
Jack Kingsman
2026-03-02 12:35:15 -08:00
parent 2581cc6af7
commit 95bacc4caf
14 changed files with 135 additions and 122 deletions
+3 -3
View File
@@ -122,13 +122,13 @@ app/
- Independent from the private `MqttPublisher` — different broker, authentication, and topic structure.
- Connects to the community broker (default `mqtt-us-v1.letsmesh.net:443`) via WebSockets over TLS.
- Authentication: Ed25519 JWT tokens signed with the radio's expanded "orlp" private key. Tokens expire after 24 hours; proactive renewal at 23 hours.
- Broker address supports `host:port` format; defaults to port 443 if omitted.
- Broker address: separate `community_mqtt_broker_host` and `community_mqtt_broker_port` fields; defaults to `mqtt-us-v1.letsmesh.net:443`.
- JWT claims include `publicKey`, `owner` (radio pubkey), `client` (app identifier), and optional `email` (for node claiming on the community aggregator).
- Topic: `meshcore/{IATA}/{pubkey}/packets` — IATA is a 3-letter region code (required to enable; no default).
- Only raw packets are published — never decrypted messages.
- Publishes are fire-and-forget. The connection loop detects publish failures via `connected` flag and reconnects within 60 seconds.
- Health endpoint includes `community_mqtt_status` field.
- Settings: `community_mqtt_enabled`, `community_mqtt_iata`, `community_mqtt_broker`, `community_mqtt_email`.
- Settings: `community_mqtt_enabled`, `community_mqtt_iata`, `community_mqtt_broker_host`, `community_mqtt_broker_port`, `community_mqtt_email`.
## API Surface (all under `/api`)
@@ -238,7 +238,7 @@ Main tables:
- `bots`
- `mqtt_broker_host`, `mqtt_broker_port`, `mqtt_username`, `mqtt_password`
- `mqtt_use_tls`, `mqtt_tls_insecure`, `mqtt_topic_prefix`, `mqtt_publish_messages`, `mqtt_publish_raw_packets`
- `community_mqtt_enabled`, `community_mqtt_iata`, `community_mqtt_broker`, `community_mqtt_email`
- `community_mqtt_enabled`, `community_mqtt_iata`, `community_mqtt_broker_host`, `community_mqtt_broker_port`, `community_mqtt_email`
## Security Posture (intentional)
+4 -15
View File
@@ -43,17 +43,6 @@ _IATA_RE = re.compile(r"^[A-Z]{3}$")
_ROUTE_MAP = {0: "F", 1: "F", 2: "D", 3: "T"}
def _parse_broker_address(broker_str: str) -> tuple[str, int]:
"""Parse 'host' or 'host:port' into (host, port). Defaults to _DEFAULT_PORT."""
if ":" in broker_str:
host, port_str = broker_str.rsplit(":", 1)
try:
return host, int(port_str)
except ValueError:
pass
return broker_str, _DEFAULT_PORT
def _base64url_encode(data: bytes) -> str:
"""Base64url encode without padding."""
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
@@ -308,8 +297,8 @@ class CommunityMqttPublisher(BaseMqttPublisher):
assert private_key is not None and public_key is not None # guaranteed by _pre_connect
pubkey_hex = public_key.hex().upper()
broker_raw = settings.community_mqtt_broker or _DEFAULT_BROKER
broker_host, broker_port = _parse_broker_address(broker_raw)
broker_host = settings.community_mqtt_broker_host or _DEFAULT_BROKER
broker_port = settings.community_mqtt_broker_port or _DEFAULT_PORT
jwt_token = _generate_jwt_token(
private_key,
public_key,
@@ -330,8 +319,8 @@ class CommunityMqttPublisher(BaseMqttPublisher):
}
def _on_connected(self, settings: AppSettings) -> tuple[str, str]:
broker_raw = settings.community_mqtt_broker or _DEFAULT_BROKER
broker_host, broker_port = _parse_broker_address(broker_raw)
broker_host = settings.community_mqtt_broker_host or _DEFAULT_BROKER
broker_port = settings.community_mqtt_broker_port or _DEFAULT_PORT
return ("Community MQTT connected", f"{broker_host}:{broker_port}")
def _on_error(self) -> tuple[str, str]:
+2 -1
View File
@@ -1916,7 +1916,8 @@ async def _migrate_032_add_community_mqtt_columns(conn: aiosqlite.Connection) ->
new_columns = [
("community_mqtt_enabled", "INTEGER DEFAULT 0"),
("community_mqtt_iata", "TEXT DEFAULT ''"),
("community_mqtt_broker", "TEXT DEFAULT 'mqtt-us-v1.letsmesh.net'"),
("community_mqtt_broker_host", "TEXT DEFAULT 'mqtt-us-v1.letsmesh.net'"),
("community_mqtt_broker_port", "INTEGER DEFAULT 443"),
("community_mqtt_email", "TEXT DEFAULT ''"),
]
+5 -1
View File
@@ -471,10 +471,14 @@ class AppSettings(BaseModel):
default="",
description="IATA region code for community MQTT topic routing (3 alpha chars)",
)
community_mqtt_broker: str = Field(
community_mqtt_broker_host: str = Field(
default="mqtt-us-v1.letsmesh.net",
description="Community MQTT broker hostname",
)
community_mqtt_broker_port: int = Field(
default=443,
description="Community MQTT broker port",
)
community_mqtt_email: str = Field(
default="",
description="Email address for node claiming on the community aggregator (optional)",
+14 -6
View File
@@ -31,7 +31,8 @@ class AppSettingsRepository:
mqtt_use_tls, mqtt_tls_insecure, mqtt_topic_prefix,
mqtt_publish_messages, mqtt_publish_raw_packets,
community_mqtt_enabled, community_mqtt_iata,
community_mqtt_broker, community_mqtt_email
community_mqtt_broker_host, community_mqtt_broker_port,
community_mqtt_email
FROM app_settings WHERE id = 1
"""
)
@@ -107,7 +108,9 @@ class AppSettingsRepository:
mqtt_publish_raw_packets=bool(row["mqtt_publish_raw_packets"]),
community_mqtt_enabled=bool(row["community_mqtt_enabled"]),
community_mqtt_iata=row["community_mqtt_iata"] or "",
community_mqtt_broker=row["community_mqtt_broker"] or "mqtt-us-v1.letsmesh.net",
community_mqtt_broker_host=row["community_mqtt_broker_host"]
or "mqtt-us-v1.letsmesh.net",
community_mqtt_broker_port=row["community_mqtt_broker_port"] or 443,
community_mqtt_email=row["community_mqtt_email"] or "",
)
@@ -133,7 +136,8 @@ class AppSettingsRepository:
mqtt_publish_raw_packets: bool | None = None,
community_mqtt_enabled: bool | None = None,
community_mqtt_iata: str | None = None,
community_mqtt_broker: str | None = None,
community_mqtt_broker_host: str | None = None,
community_mqtt_broker_port: int | None = None,
community_mqtt_email: str | None = None,
) -> AppSettings:
"""Update app settings. Only provided fields are updated."""
@@ -222,9 +226,13 @@ class AppSettingsRepository:
updates.append("community_mqtt_iata = ?")
params.append(community_mqtt_iata)
if community_mqtt_broker is not None:
updates.append("community_mqtt_broker = ?")
params.append(community_mqtt_broker)
if community_mqtt_broker_host is not None:
updates.append("community_mqtt_broker_host = ?")
params.append(community_mqtt_broker_host)
if community_mqtt_broker_port is not None:
updates.append("community_mqtt_broker_port = ?")
params.append(community_mqtt_broker_port)
if community_mqtt_email is not None:
updates.append("community_mqtt_email = ?")
+13 -3
View File
@@ -106,10 +106,16 @@ class AppSettingsUpdate(BaseModel):
default=None,
description="IATA region code for community MQTT topic routing (3 alpha chars)",
)
community_mqtt_broker: str | None = Field(
community_mqtt_broker_host: str | None = Field(
default=None,
description="Community MQTT broker hostname",
)
community_mqtt_broker_port: int | None = Field(
default=None,
ge=1,
le=65535,
description="Community MQTT broker port",
)
community_mqtt_email: str | None = Field(
default=None,
description="Email address for node claiming on the community aggregator",
@@ -214,8 +220,12 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
kwargs["community_mqtt_iata"] = iata
community_mqtt_changed = True
if update.community_mqtt_broker is not None:
kwargs["community_mqtt_broker"] = update.community_mqtt_broker
if update.community_mqtt_broker_host is not None:
kwargs["community_mqtt_broker_host"] = update.community_mqtt_broker_host
community_mqtt_changed = True
if update.community_mqtt_broker_port is not None:
kwargs["community_mqtt_broker_port"] = update.community_mqtt_broker_port
community_mqtt_changed = True
if update.community_mqtt_email is not None: