Server side message decryption and api docs

This commit is contained in:
ajvpot
2025-09-06 03:10:34 +02:00
parent 3d9f2cca21
commit 8a4b591776
4 changed files with 624 additions and 12 deletions
+439
View File
@@ -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>
);
}
+37 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}