From da4006d230facd7766fbdabe557cedd4b0d6cc7f Mon Sep 17 00:00:00 2001 From: Lloyd Date: Sun, 16 Nov 2025 20:56:29 +0000 Subject: [PATCH] feat: add transport key management methods and API endpoints --- repeater/data_acquisition/sqlite_handler.py | 127 +++++++++++++++++- .../data_acquisition/storage_collector.py | 17 ++- repeater/web/api_endpoints.py | 103 +++++++++++++- 3 files changed, 244 insertions(+), 3 deletions(-) diff --git a/repeater/data_acquisition/sqlite_handler.py b/repeater/data_acquisition/sqlite_handler.py index 9fce559..f7e0dfb 100644 --- a/repeater/data_acquisition/sqlite_handler.py +++ b/repeater/data_acquisition/sqlite_handler.py @@ -73,6 +73,20 @@ class SQLiteHandler: ) """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS transport_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + flood_policy TEXT NOT NULL CHECK (flood_policy IN ('allow', 'deny')), + transport_key TEXT NOT NULL, + last_used REAL, + parent_id INTEGER, + created_at REAL NOT NULL, + updated_at REAL NOT NULL, + FOREIGN KEY (parent_id) REFERENCES transport_keys(id) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_packets_timestamp ON packets(timestamp)") conn.execute("CREATE INDEX IF NOT EXISTS idx_packets_type ON packets(type)") conn.execute("CREATE INDEX IF NOT EXISTS idx_packets_hash ON packets(packet_hash)") @@ -80,6 +94,8 @@ class SQLiteHandler: conn.execute("CREATE INDEX IF NOT EXISTS idx_adverts_timestamp ON adverts(timestamp)") conn.execute("CREATE INDEX IF NOT EXISTS idx_adverts_pubkey ON adverts(pubkey)") conn.execute("CREATE INDEX IF NOT EXISTS idx_noise_timestamp ON noise_floor(timestamp)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_transport_keys_name ON transport_keys(name)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_transport_keys_parent ON transport_keys(parent_id)") conn.commit() logger.info(f"SQLite database initialized: {self.sqlite_path}") @@ -646,4 +662,113 @@ class SQLiteHandler: except Exception as e: logger.error(f"Failed to get adverts by contact_type '{contact_type}': {e}") - return [] \ No newline at end of file + return [] + + def create_transport_key(self, name: str, flood_policy: str, transport_key: str, parent_id: Optional[int] = None, last_used: Optional[float] = None) -> Optional[int]: + try: + current_time = time.time() + with sqlite3.connect(self.sqlite_path) as conn: + cursor = conn.execute(""" + INSERT INTO transport_keys (name, flood_policy, transport_key, parent_id, last_used, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (name, flood_policy, transport_key, parent_id, last_used, current_time, current_time)) + return cursor.lastrowid + except Exception as e: + logger.error(f"Failed to create transport key: {e}") + return None + + def get_transport_keys(self) -> List[dict]: + try: + with sqlite3.connect(self.sqlite_path) as conn: + conn.row_factory = sqlite3.Row + rows = conn.execute(""" + SELECT id, name, flood_policy, transport_key, parent_id, last_used, created_at, updated_at + FROM transport_keys + ORDER BY created_at ASC + """).fetchall() + + return [{ + "id": row["id"], + "name": row["name"], + "flood_policy": row["flood_policy"], + "transport_key": row["transport_key"], + "parent_id": row["parent_id"], + "last_used": row["last_used"], + "created_at": row["created_at"], + "updated_at": row["updated_at"] + } for row in rows] + except Exception as e: + logger.error(f"Failed to get transport keys: {e}") + return [] + + def get_transport_key_by_id(self, key_id: int) -> Optional[dict]: + try: + with sqlite3.connect(self.sqlite_path) as conn: + conn.row_factory = sqlite3.Row + row = conn.execute(""" + SELECT id, name, flood_policy, transport_key, parent_id, last_used, created_at, updated_at + FROM transport_keys WHERE id = ? + """, (key_id,)).fetchone() + + if row: + return { + "id": row["id"], + "name": row["name"], + "flood_policy": row["flood_policy"], + "transport_key": row["transport_key"], + "parent_id": row["parent_id"], + "last_used": row["last_used"], + "created_at": row["created_at"], + "updated_at": row["updated_at"] + } + return None + except Exception as e: + logger.error(f"Failed to get transport key by id: {e}") + return None + + def update_transport_key(self, key_id: int, name: Optional[str] = None, flood_policy: Optional[str] = None, transport_key: Optional[str] = None, parent_id: Optional[int] = None, last_used: Optional[float] = None) -> bool: + try: + updates = [] + params = [] + + if name is not None: + updates.append("name = ?") + params.append(name) + if flood_policy is not None: + updates.append("flood_policy = ?") + params.append(flood_policy) + if transport_key is not None: + updates.append("transport_key = ?") + params.append(transport_key) + if parent_id is not None: + updates.append("parent_id = ?") + params.append(parent_id) + if last_used is not None: + updates.append("last_used = ?") + params.append(last_used) + + if not updates: + return False + + updates.append("updated_at = ?") + params.append(time.time()) + params.append(key_id) + + with sqlite3.connect(self.sqlite_path) as conn: + cursor = conn.execute(f""" + UPDATE transport_keys SET {', '.join(updates)} + WHERE id = ? + """, params) + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"Failed to update transport key: {e}") + return False + + def delete_transport_key(self, key_id: int) -> bool: + try: + with sqlite3.connect(self.sqlite_path) as conn: + cursor = conn.execute("DELETE FROM transport_keys WHERE id = ?", (key_id,)) + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"Failed to delete transport key: {e}") + return False \ No newline at end of file diff --git a/repeater/data_acquisition/storage_collector.py b/repeater/data_acquisition/storage_collector.py index cf88cd8..793d07c 100644 --- a/repeater/data_acquisition/storage_collector.py +++ b/repeater/data_acquisition/storage_collector.py @@ -91,4 +91,19 @@ class StorageCollector: return self.sqlite_handler.get_noise_floor_stats(hours) def close(self): - self.mqtt_handler.close() \ No newline at end of file + self.mqtt_handler.close() + + def create_transport_key(self, name: str, flood_policy: str, transport_key: str, parent_id: Optional[int] = None, last_used: Optional[float] = None) -> Optional[int]: + return self.sqlite_handler.create_transport_key(name, flood_policy, transport_key, parent_id, last_used) + + def get_transport_keys(self) -> list: + return self.sqlite_handler.get_transport_keys() + + def get_transport_key_by_id(self, key_id: int) -> Optional[dict]: + return self.sqlite_handler.get_transport_key_by_id(key_id) + + def update_transport_key(self, key_id: int, name: Optional[str] = None, flood_policy: Optional[str] = None, transport_key: Optional[str] = None, parent_id: Optional[int] = None, last_used: Optional[float] = None) -> bool: + return self.sqlite_handler.update_transport_key(key_id, name, flood_policy, transport_key, parent_id, last_used) + + def delete_transport_key(self, key_id: int) -> bool: + return self.sqlite_handler.delete_transport_key(key_id) \ No newline at end of file diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 8d2ed4b..142d922 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -680,4 +680,105 @@ class APIEndpoints: return self._error(f"Invalid parameter format: {e}") except Exception as e: logger.error(f"Error getting adverts by contact type: {e}") - return self._error(e) \ No newline at end of file + return self._error(e) + + @cherrypy.expose + @cherrypy.tools.json_out() + @cors_enabled + def transport_keys(self): + if cherrypy.request.method == "GET": + try: + storage = self._get_storage() + keys = storage.get_transport_keys() + return self._success(keys, count=len(keys)) + except Exception as e: + logger.error(f"Error getting transport keys: {e}") + return self._error(e) + + elif cherrypy.request.method == "POST": + try: + data = cherrypy.request.json or {} + name = data.get("name") + flood_policy = data.get("flood_policy") + transport_key = data.get("transport_key") + parent_id = data.get("parent_id") + last_used = data.get("last_used") + + if not name or not flood_policy or not transport_key: + return self._error("Missing required fields: name, flood_policy, transport_key") + + if flood_policy not in ["allow", "deny"]: + return self._error("flood_policy must be 'allow' or 'deny'") + + storage = self._get_storage() + key_id = storage.create_transport_key(name, flood_policy, transport_key, parent_id, last_used) + + if key_id: + return self._success({"id": key_id}, message="Transport key created successfully") + else: + return self._error("Failed to create transport key") + except Exception as e: + logger.error(f"Error creating transport key: {e}") + return self._error(e) + + @cherrypy.expose + @cherrypy.tools.json_out() + @cors_enabled + def transport_key(self, key_id): + if cherrypy.request.method == "GET": + try: + key_id = int(key_id) + storage = self._get_storage() + key = storage.get_transport_key_by_id(key_id) + if key: + return self._success(key) + else: + return self._error("Transport key not found") + except ValueError: + return self._error("Invalid key_id format") + except Exception as e: + logger.error(f"Error getting transport key: {e}") + return self._error(e) + + elif cherrypy.request.method == "PUT": + try: + key_id = int(key_id) + data = cherrypy.request.json or {} + + name = data.get("name") + flood_policy = data.get("flood_policy") + transport_key = data.get("transport_key") + parent_id = data.get("parent_id") + last_used = data.get("last_used") + + if flood_policy and flood_policy not in ["allow", "deny"]: + return self._error("flood_policy must be 'allow' or 'deny'") + + storage = self._get_storage() + success = storage.update_transport_key(key_id, name, flood_policy, transport_key, parent_id, last_used) + + if success: + return self._success({"id": key_id}, message="Transport key updated successfully") + else: + return self._error("Failed to update transport key or key not found") + except ValueError: + return self._error("Invalid key_id format") + except Exception as e: + logger.error(f"Error updating transport key: {e}") + return self._error(e) + + elif cherrypy.request.method == "DELETE": + try: + key_id = int(key_id) + storage = self._get_storage() + success = storage.delete_transport_key(key_id) + + if success: + return self._success({"id": key_id}, message="Transport key deleted successfully") + else: + return self._error("Failed to delete transport key or key not found") + except ValueError: + return self._error("Invalid key_id format") + except Exception as e: + logger.error(f"Error deleting transport key: {e}") + return self._error(e) \ No newline at end of file