mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-03-28 17:42:58 +01:00
no sqli pls
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "meshexplorer",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^1.11.2",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@types/leaflet": "^1.9.19",
|
||||
@@ -62,6 +63,22 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@clickhouse/client": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.11.2.tgz",
|
||||
"integrity": "sha512-ZE7Q1qxsDNXCkGPf1zqmhpZpwAKxKT+1s4Z432J1Mb2Gm26Y4tG/sJoug81AfAJTt6s7taO2vzNBAKfSR3SStg==",
|
||||
"dependencies": {
|
||||
"@clickhouse/client-common": "1.11.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@clickhouse/client-common": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@clickhouse/client-common/-/client-common-1.11.2.tgz",
|
||||
"integrity": "sha512-H4ECHqaipzMgiZKqpb1Z4N3Ofq+lVTCn8I59XsSynqrsfR4jWZD3PipXVvIzMpDmTMvrlJWrOwAdm0DMNiMQbA=="
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^1.11.2",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@types/leaflet": "^1.9.19",
|
||||
|
||||
@@ -16,7 +16,7 @@ const DEFAULT = {
|
||||
};
|
||||
|
||||
type NodePosition = {
|
||||
from_node_id: string;
|
||||
node_id: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
altitude?: number;
|
||||
@@ -56,10 +56,14 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) {
|
||||
});
|
||||
const marker = L.marker([node.latitude, node.longitude], { icon });
|
||||
(marker as any).options.nodeData = node;
|
||||
let popupHtml = `<div><div><b>ID:</b> ${node.from_node_id}</div><div><b>Lat:</b> ${node.latitude}</div><div><b>Lng:</b> ${node.longitude}</div>`;
|
||||
if (node.altitude !== undefined) popupHtml += `<div><b>Alt:</b> ${node.altitude}</div>`;
|
||||
if (node.last_seen) popupHtml += `<div><b>Last seen:</b> ${node.last_seen}</div>`;
|
||||
if (node.type) popupHtml += `<div><b>Type:</b> ${node.type}</div>`;
|
||||
let popupHtml = `<div>`;
|
||||
popupHtml += `<div><b>ID:</b> ${node.node_id}</div>`;
|
||||
popupHtml += `<div><b>Short Name:</b> ${node.short_name ?? "-"}</div>`;
|
||||
popupHtml += `<div><b>Type:</b> ${node.type ?? "-"}</div>`;
|
||||
popupHtml += `<div><b>Lat:</b> ${node.latitude}</div>`;
|
||||
popupHtml += `<div><b>Lng:</b> ${node.longitude}</div>`;
|
||||
popupHtml += `<div><b>Alt:</b> ${node.altitude !== undefined ? node.altitude : "-"}</div>`;
|
||||
popupHtml += `<div><b>Last seen:</b> ${node.last_seen ?? "-"}</div>`;
|
||||
popupHtml += `</div>`;
|
||||
marker.bindPopup(popupHtml);
|
||||
marker.addTo(map);
|
||||
@@ -155,10 +159,14 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) {
|
||||
});
|
||||
const marker = L.marker([node.latitude, node.longitude], { icon });
|
||||
(marker as any).options.nodeData = node;
|
||||
let popupHtml = `<div><div><b>ID:</b> ${node.from_node_id}</div><div><b>Lat:</b> ${node.latitude}</div><div><b>Lng:</b> ${node.longitude}</div>`;
|
||||
if (node.altitude !== undefined) popupHtml += `<div><b>Alt:</b> ${node.altitude}</div>`;
|
||||
if (node.last_seen) popupHtml += `<div><b>Last seen:</b> ${node.last_seen}</div>`;
|
||||
if (node.type) popupHtml += `<div><b>Type:</b> ${node.type}</div>`;
|
||||
let popupHtml = `<div>`;
|
||||
popupHtml += `<div><b>ID:</b> ${node.node_id}</div>`;
|
||||
popupHtml += `<div><b>Short Name:</b> ${node.short_name ?? "-"}</div>`;
|
||||
popupHtml += `<div><b>Type:</b> ${node.type ?? "-"}</div>`;
|
||||
popupHtml += `<div><b>Lat:</b> ${node.latitude}</div>`;
|
||||
popupHtml += `<div><b>Lng:</b> ${node.longitude}</div>`;
|
||||
popupHtml += `<div><b>Alt:</b> ${node.altitude !== undefined ? node.altitude : "-"}</div>`;
|
||||
popupHtml += `<div><b>Last seen:</b> ${node.last_seen ?? "-"}</div>`;
|
||||
popupHtml += `</div>`;
|
||||
marker.bindPopup(popupHtml);
|
||||
markers.addLayer(marker);
|
||||
@@ -177,6 +185,7 @@ export default function MapView() {
|
||||
const [nodePositions, setNodePositions] = useState<NodePosition[]>([]);
|
||||
const [bounds, setBounds] = useState<[[number, number], [number, number]] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lastResultCount, setLastResultCount] = useState<number>(0);
|
||||
const fetchController = useRef<AbortController | null>(null);
|
||||
const lastRequestedBounds = useRef<[[number, number], [number, number]] | null>(null);
|
||||
const { config } = useConfig ? useConfig() : { config: undefined };
|
||||
@@ -231,7 +240,10 @@ export default function MapView() {
|
||||
fetch(url, { signal: controller.signal })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) setNodePositions(data);
|
||||
if (Array.isArray(data)) {
|
||||
setNodePositions(data);
|
||||
setLastResultCount(data.length);
|
||||
}
|
||||
if (fetchController.current === controller) setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -255,7 +267,7 @@ export default function MapView() {
|
||||
useMapEvents({
|
||||
moveend: (e) => {
|
||||
const b = e.target.getBounds();
|
||||
const buffer = 0.2; // 20% buffer
|
||||
const buffer = 0.05; // 5% buffer
|
||||
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
|
||||
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
|
||||
const newBounds: [[number, number], [number, number]] = [
|
||||
@@ -268,13 +280,18 @@ export default function MapView() {
|
||||
b.getNorthEast().lng + lngDiff * buffer,
|
||||
],
|
||||
];
|
||||
if (!lastRequestedBounds.current || !isBoundsInside(newBounds, lastRequestedBounds.current)) {
|
||||
// Only always refetch if we have too many nodes depending on clustering setting.
|
||||
if (
|
||||
(lastResultCount > (config?.clustering ? 5000: 1000)) ||
|
||||
!lastRequestedBounds.current ||
|
||||
!isBoundsInside(newBounds, lastRequestedBounds.current)
|
||||
) {
|
||||
setBounds(newBounds);
|
||||
}
|
||||
},
|
||||
zoomend: (e) => {
|
||||
const b = e.target.getBounds();
|
||||
const buffer = 0.2; // 20% buffer
|
||||
const buffer = 0.05; // 5% buffer
|
||||
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
|
||||
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
|
||||
const newBounds: [[number, number], [number, number]] = [
|
||||
@@ -287,7 +304,12 @@ export default function MapView() {
|
||||
b.getNorthEast().lng + lngDiff * buffer,
|
||||
],
|
||||
];
|
||||
if (!lastRequestedBounds.current || !isBoundsInside(newBounds, lastRequestedBounds.current)) {
|
||||
// Only always refetch if clustering is disabled and lastResultCount > 1000
|
||||
if (
|
||||
(config?.clustering === false && lastResultCount > 1000) ||
|
||||
!lastRequestedBounds.current ||
|
||||
!isBoundsInside(newBounds, lastRequestedBounds.current)
|
||||
) {
|
||||
setBounds(newBounds);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,19 +6,34 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
|
||||
"latitude IS NOT NULL",
|
||||
"longitude IS NOT NULL"
|
||||
];
|
||||
if (minLat) where.push(`latitude >= ${minLat}`);
|
||||
if (maxLat) where.push(`latitude <= ${maxLat}`);
|
||||
if (minLng) where.push(`longitude >= ${minLng}`);
|
||||
if (maxLng) where.push(`longitude <= ${maxLng}`);
|
||||
const params: Record<string, any> = {};
|
||||
if (minLat !== null && minLat !== undefined && minLat !== "") {
|
||||
where.push(`latitude >= {minLat:Float64}`);
|
||||
params.minLat = Number(minLat);
|
||||
}
|
||||
if (maxLat !== null && maxLat !== undefined && maxLat !== "") {
|
||||
where.push(`latitude <= {maxLat:Float64}`);
|
||||
params.maxLat = Number(maxLat);
|
||||
}
|
||||
if (minLng !== null && minLng !== undefined && minLng !== "") {
|
||||
where.push(`longitude >= {minLng:Float64}`);
|
||||
params.minLng = Number(minLng);
|
||||
}
|
||||
if (maxLng !== null && maxLng !== undefined && maxLng !== "") {
|
||||
where.push(`longitude <= {maxLng:Float64}`);
|
||||
params.maxLng = Number(maxLng);
|
||||
}
|
||||
if (nodeTypes && nodeTypes.length > 0) {
|
||||
const types = nodeTypes.map(t => `'${t}'`).join(",");
|
||||
where.push(`type IN (${types})`);
|
||||
where.push(`type IN {nodeTypes:Array(String)}`);
|
||||
params.nodeTypes = nodeTypes;
|
||||
}
|
||||
if (lastSeen !== null && lastSeen !== undefined && lastSeen !== "") {
|
||||
where.push(`last_seen >= now() - INTERVAL ${lastSeen} SECOND`);
|
||||
where.push(`last_seen >= now() - INTERVAL {lastSeen:UInt32} SECOND`);
|
||||
params.lastSeen = Number(lastSeen);
|
||||
}
|
||||
const query = `SELECT node_id, name, short_name, latitude, longitude, last_seen, type FROM unified_latest_nodeinfo WHERE ${where.join(" AND ")}`;
|
||||
const rows = await clickhouse.query(query).toPromise();
|
||||
const resultSet = await clickhouse.query({ query, query_params: params, format: 'JSONEachRow' });
|
||||
const rows = await resultSet.json();
|
||||
return rows as Array<{
|
||||
node_id: string;
|
||||
name?: string | null;
|
||||
@@ -28,4 +43,4 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
|
||||
last_seen: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { ClickHouse } from 'clickhouse';
|
||||
import { createClient } from '@clickhouse/client';
|
||||
|
||||
const host = process.env.CLICKHOUSE_HOST || 'localhost';
|
||||
const port = process.env.CLICKHOUSE_PORT || '8123';
|
||||
const user = process.env.CLICKHOUSE_USER || 'default';
|
||||
const password = process.env.CLICKHOUSE_PASSWORD || 'password';
|
||||
|
||||
export const clickhouse = new ClickHouse({
|
||||
url: `http://${host}`,
|
||||
port: Number(port),
|
||||
basicAuth: { username: user, password },
|
||||
isUseGzip: false,
|
||||
format: 'json',
|
||||
export const clickhouse = createClient({
|
||||
host: `http://${host}:${port}`,
|
||||
username: user,
|
||||
password: password,
|
||||
// You can add more options as needed, e.g. database, compression, etc.
|
||||
});
|
||||
Reference in New Issue
Block a user