mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-08-07 17:12:43 +02:00
Server side message decryption and api docs
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ApiDocsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-neutral-800 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="bg-white dark:bg-neutral-900 shadow rounded-lg">
|
||||
<div className="px-6 py-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">MeshExplorer API Documentation</h1>
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-400 dark:border-yellow-500 p-4 mb-8">
|
||||
<div className="flex">
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||
<strong>Note:</strong> This documentation may be out of date as the project is continuously improving.
|
||||
For the most up-to-date information, please refer to the source code or contact the development team.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-12">
|
||||
{/* Chat API */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Chat API</h2>
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">GET /api/chat</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Retrieve chat messages from the mesh network.</p>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Query Parameters</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4 text-sm">Note: Array parameters like <code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded text-gray-800 dark:text-gray-200">privateKeys</code> can be specified multiple times in the URL (e.g., <code className="bg-gray-200 dark:bg-neutral-700 px-1 rounded text-gray-800 dark:text-gray-200">?privateKeys=key1&privateKeys=key2</code>).</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Parameter</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Required</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">limit</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">number</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Number of messages to return (default: 20)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">before</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Get messages before this timestamp</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">after</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Get messages after this timestamp</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">channel_id</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by specific channel hash</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">region</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by geographic region</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">decrypt</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">boolean</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Enable message decryption (any value present)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">privateKeys</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string[]</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Array of private keys for decryption (can be specified multiple times)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2 mt-6">Response Format</h4>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-green-400 dark:text-green-300 text-sm">
|
||||
{`// Without decryption
|
||||
[
|
||||
{
|
||||
"ingest_timestamp": "2024-01-01T12:00:00Z",
|
||||
"mesh_timestamp": "2024-01-01T12:00:00Z",
|
||||
"channel_hash": "a1",
|
||||
"mac": "b2c3",
|
||||
"encrypted_message": "hex_encoded_data",
|
||||
"message_count": 123,
|
||||
"origin_key_path_array": [["origin", "pubkey", "path"]]
|
||||
}
|
||||
]
|
||||
|
||||
// With decryption (decrypt=true)
|
||||
[
|
||||
{
|
||||
"ingest_timestamp": "2024-01-01T12:00:00Z",
|
||||
"mesh_timestamp": "2024-01-01T12:00:00Z",
|
||||
"channel_hash": "a1",
|
||||
"mac": "b2c3",
|
||||
"encrypted_message": "hex_encoded_data",
|
||||
"message_count": 123,
|
||||
"origin_key_path_array": [["origin", "pubkey", "path"]],
|
||||
"decrypted": {
|
||||
"timestamp": 1704110400,
|
||||
"msgType": 1,
|
||||
"sender": "username",
|
||||
"text": "Hello world!",
|
||||
"rawText": "username: Hello world!"
|
||||
}
|
||||
}
|
||||
]`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Map API */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Map API</h2>
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">GET /api/map</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Retrieve node positions for map visualization.</p>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Query Parameters</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Parameter</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Required</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">minLat</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">number</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Minimum latitude for bounding box</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">maxLat</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">number</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Maximum latitude for bounding box</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">minLng</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">number</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Minimum longitude for bounding box</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">maxLng</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">number</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Maximum longitude for bounding box</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">nodeTypes</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string[]</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by node types (can be specified multiple times)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">lastSeen</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">number</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by last seen time in seconds</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2 mt-6">Response Format</h4>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-green-400 dark:text-green-300 text-sm">
|
||||
{`[
|
||||
{
|
||||
"node_id": "abc123...",
|
||||
"name": "Node Name",
|
||||
"short_name": "NODE",
|
||||
"latitude": 40.7128,
|
||||
"longitude": -74.0060,
|
||||
"last_seen": "2024-01-01T12:00:00Z",
|
||||
"first_seen": "2024-01-01T10:00:00Z",
|
||||
"type": "meshcore"
|
||||
}
|
||||
]`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats APIs */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Stats APIs</h2>
|
||||
|
||||
{/* Total Nodes */}
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">GET /api/stats/total-nodes</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Get the total number of nodes in the network.</p>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Query Parameters</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Parameter</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Required</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">region</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by geographic region</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2 mt-6">Response Format</h4>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-green-400 dark:text-green-300 text-sm">
|
||||
{`{
|
||||
"total_nodes": 1234
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nodes Over Time */}
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">GET /api/stats/nodes-over-time</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Get cumulative node statistics over time with a 7-day rolling window.</p>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Query Parameters</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Parameter</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Required</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">region</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by geographic region</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2 mt-6">Response Format</h4>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-green-400 dark:text-green-300 text-sm">
|
||||
{`{
|
||||
"data": [
|
||||
{
|
||||
"day": "2024-01-01",
|
||||
"cumulative_unique_nodes": 100,
|
||||
"nodes_with_location": 80,
|
||||
"nodes_without_location": 20,
|
||||
"repeaters": 15,
|
||||
"room_servers": 5
|
||||
}
|
||||
]
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Popular Channels */}
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">GET /api/stats/popular-channels</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Get the most popular channels by message count (top 10).</p>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Query Parameters</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Parameter</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Required</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">region</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by geographic region</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2 mt-6">Response Format</h4>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-green-400 dark:text-green-300 text-sm">
|
||||
{`{
|
||||
"data": [
|
||||
{
|
||||
"channel_hash": "a1",
|
||||
"message_count": 1234
|
||||
}
|
||||
]
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Repeater Prefixes */}
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">GET /api/stats/repeater-prefixes</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Get repeater node statistics grouped by public key prefix (first 2 characters).</p>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Query Parameters</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-neutral-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Parameter</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Required</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">region</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">No</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Filter by geographic region</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2 mt-6">Response Format</h4>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-green-400 dark:text-green-300 text-sm">
|
||||
{`{
|
||||
"data": [
|
||||
{
|
||||
"prefix": "ab",
|
||||
"node_count": 15,
|
||||
"node_names": ["Node1", "Node2", "Node3"]
|
||||
}
|
||||
]
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Error Handling */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Error Handling</h2>
|
||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">All API endpoints return appropriate HTTP status codes and error messages:</p>
|
||||
<ul className="list-disc list-inside text-gray-600 dark:text-gray-300 space-y-2">
|
||||
<li><strong>200 OK:</strong> Successful request</li>
|
||||
<li><strong>500 Internal Server Error:</strong> Server error with descriptive message</li>
|
||||
</ul>
|
||||
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2 mt-4">Error Response Format</h4>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-red-400 dark:text-red-300 text-sm">
|
||||
{`{
|
||||
"error": "Failed to fetch data"
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Examples */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Example Requests</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6">
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Get recent chat messages with decryption</h3>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-blue-400 dark:text-blue-300 text-sm">
|
||||
{`GET /api/chat?decrypt=true&limit=10&privateKeys=key1&privateKeys=key2`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6">
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Get nodes in a specific region</h3>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-blue-400 dark:text-blue-300 text-sm">
|
||||
{`GET /api/map?minLat=40.0&maxLat=41.0&minLng=-74.0&maxLng=-73.0&nodeTypes=meshcore`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-6">
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100 mb-2">Get total nodes in a region</h3>
|
||||
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-blue-400 dark:text-blue-300 text-sm">
|
||||
{`GET /api/stats/total-nodes?region=us-east`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getLatestChatMessages } from "@/lib/clickhouse/actions";
|
||||
import { getChannelIdFromKey } from "@/lib/meshcore";
|
||||
import { getChannelIdFromKey, decryptMeshcoreGroupMessage } from "@/lib/meshcore";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
@@ -10,7 +10,43 @@ export async function GET(req: Request) {
|
||||
const after = searchParams.get("after") || undefined;
|
||||
const channelId = searchParams.get("channel_id") || undefined;
|
||||
const region = searchParams.get("region") || undefined;
|
||||
const decrypt = searchParams.has("decrypt");
|
||||
const privateKeys = searchParams.getAll("privateKeys");
|
||||
|
||||
// Always include the public meshcore key
|
||||
const PUBLIC_MESHCORE_KEY = "izOH6cXN6mrJ5e26oRXNcg==";
|
||||
const allKeys = [PUBLIC_MESHCORE_KEY, ...privateKeys];
|
||||
|
||||
const messages = await getLatestChatMessages({ limit, before, after, channelId, region } as { limit?: number, before?: string, after?: string, channelId?: string, region?: string });
|
||||
|
||||
if (decrypt) {
|
||||
const decryptedMessages = [];
|
||||
|
||||
for (const message of messages) {
|
||||
try {
|
||||
const decrypted = await decryptMeshcoreGroupMessage({
|
||||
encrypted_message: message.encrypted_message,
|
||||
mac: message.mac,
|
||||
channel_hash: message.channel_hash,
|
||||
knownKeys: allKeys,
|
||||
parse: true
|
||||
});
|
||||
|
||||
if (decrypted) {
|
||||
decryptedMessages.push({
|
||||
...message,
|
||||
decrypted
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip messages that fail to decrypt
|
||||
console.warn("Failed to decrypt message:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(decryptedMessages);
|
||||
}
|
||||
|
||||
return NextResponse.json(messages);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to fetch chat messages" }, { status: 500 });
|
||||
|
||||
+140
-7
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { Cog6ToothIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Cog6ToothIcon, InformationCircleIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { useConfig } from "./ConfigContext";
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import InfoModal from "./InfoModal";
|
||||
import { getAppName } from "../lib/api";
|
||||
|
||||
@@ -10,19 +10,152 @@ interface HeaderProps {
|
||||
configButtonRef?: React.Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
export default function Header({ configButtonRef }: HeaderProps) {
|
||||
const { openConfig, configButtonRef: contextButtonRef } = useConfig();
|
||||
const [infoModalOpen, setInfoModalOpen] = useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [visibleItems, setVisibleItems] = useState<NavItem[]>([]);
|
||||
const [hiddenItems, setHiddenItems] = useState<NavItem[]>([]);
|
||||
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
const itemsRef = useRef<HTMLDivElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Define all navigation items
|
||||
const allNavItems: NavItem[] = [
|
||||
{ href: "/messages", label: "Messages" },
|
||||
{ href: "/stats", label: "Stats" },
|
||||
{ href: "/api-docs", label: "API Docs" },
|
||||
];
|
||||
|
||||
// Measure available space and determine which items can fit
|
||||
const measureAndLayout = useCallback(() => {
|
||||
if (!navRef.current || !itemsRef.current) return;
|
||||
|
||||
const navWidth = navRef.current.offsetWidth;
|
||||
const rightSectionWidth = 200; // Approximate width for buttons
|
||||
const availableWidth = navWidth - rightSectionWidth - 48; // 48px for padding
|
||||
|
||||
// Create temporary elements to measure item widths
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.style.position = 'absolute';
|
||||
tempContainer.style.visibility = 'hidden';
|
||||
tempContainer.style.whiteSpace = 'nowrap';
|
||||
tempContainer.className = 'flex gap-6 items-center';
|
||||
document.body.appendChild(tempContainer);
|
||||
|
||||
// Measure all items first to get their widths
|
||||
const itemWidths: number[] = [];
|
||||
for (const item of allNavItems) {
|
||||
const tempItem = document.createElement('a');
|
||||
tempItem.href = item.href;
|
||||
tempItem.textContent = item.label;
|
||||
tempItem.className = 'text-gray-800 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors';
|
||||
tempContainer.appendChild(tempItem);
|
||||
itemWidths.push(tempItem.offsetWidth);
|
||||
}
|
||||
|
||||
document.body.removeChild(tempContainer);
|
||||
|
||||
// Find the cutoff point while preserving order
|
||||
let currentWidth = 0;
|
||||
let cutoffIndex = allNavItems.length;
|
||||
|
||||
for (let i = 0; i < allNavItems.length; i++) {
|
||||
const itemWidth = itemWidths[i];
|
||||
const gapWidth = i > 0 ? 24 : 0; // 24px gap between items
|
||||
|
||||
if (currentWidth + itemWidth + gapWidth <= availableWidth) {
|
||||
currentWidth += itemWidth + gapWidth;
|
||||
} else {
|
||||
cutoffIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Split items at the cutoff point to preserve order
|
||||
const visible = allNavItems.slice(0, cutoffIndex);
|
||||
const hidden = allNavItems.slice(cutoffIndex);
|
||||
|
||||
setVisibleItems(visible);
|
||||
setHiddenItems(hidden);
|
||||
}, []);
|
||||
|
||||
// Handle window resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
measureAndLayout();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
measureAndLayout();
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [measureAndLayout]);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="w-full flex items-center justify-between px-6 py-3 text-gray-800 dark:text-gray-100 bg-white dark:bg-neutral-900 shadow z-20">
|
||||
<nav className="flex gap-6 items-center">
|
||||
<Link href="/" className="font-bold text-lg">{getAppName()}</Link>
|
||||
<Link href="/messages">Messages</Link>
|
||||
<Link href="/stats">Stats</Link>
|
||||
<nav ref={navRef} className="flex gap-6 items-center flex-1">
|
||||
<Link href="/" className="font-bold text-lg flex-shrink-0">{getAppName()}</Link>
|
||||
<div ref={itemsRef} className="flex gap-6 items-center">
|
||||
{visibleItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-gray-800 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors whitespace-nowrap"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
{hiddenItems.length > 0 && (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="flex items-center gap-1 text-gray-800 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||
aria-label="More navigation options"
|
||||
>
|
||||
More
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</button>
|
||||
{dropdownOpen && (
|
||||
<div className="absolute top-full left-0 mt-2 w-48 bg-white dark:bg-neutral-800 rounded-md shadow-lg border border-gray-200 dark:border-neutral-700 z-30">
|
||||
{hiddenItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="block px-4 py-2 text-sm text-gray-800 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-neutral-700 transition-colors"
|
||||
onClick={() => setDropdownOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setInfoModalOpen(true)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||
|
||||
+8
-4
@@ -1,4 +1,4 @@
|
||||
import { createHash } from "crypto";
|
||||
import { createHash, createHmac } from "crypto";
|
||||
import aesjs from "aes-js";
|
||||
|
||||
// Module-level cache for channel IDs
|
||||
@@ -69,15 +69,19 @@ export function getChannelIdFromKey(key: string): string {
|
||||
|
||||
// Helper: HMAC-SHA256, returns Uint8Array
|
||||
async function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
|
||||
if (window.crypto?.subtle) {
|
||||
// Check if we're in a browser environment
|
||||
if (typeof window !== 'undefined' && window.crypto?.subtle) {
|
||||
// Use WebCrypto API in browser
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
"raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
|
||||
);
|
||||
const sig = await window.crypto.subtle.sign("HMAC", cryptoKey, data);
|
||||
return new Uint8Array(sig);
|
||||
} else {
|
||||
// Fallback: use a JS polyfill if needed (not implemented here)
|
||||
throw new Error("No WebCrypto support for HMAC-SHA256");
|
||||
// Use Node.js crypto in server environment
|
||||
const hmac = createHmac('sha256', Buffer.from(key));
|
||||
hmac.update(Buffer.from(data));
|
||||
return new Uint8Array(hmac.digest());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user