connections page: update API, combine opnsense data with db data
This commit is contained in:
parent
76559d2931
commit
29fbccc953
@ -88,7 +88,7 @@
|
|||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
--surface: 217.2 40.6% 10.5%;
|
--surface: 217.2 40.6% 11.5%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
src/lib/connections.ts
Normal file
10
src/lib/connections.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type ConnectionDetails = {
|
||||||
|
deviceId: number;
|
||||||
|
deviceName: string;
|
||||||
|
devicePublicKey: string;
|
||||||
|
deviceIps: string[];
|
||||||
|
endpoint: string;
|
||||||
|
transferRx: number;
|
||||||
|
transferTx: number;
|
||||||
|
latestHandshake: number;
|
||||||
|
};
|
@ -2,11 +2,54 @@ import { error } from '@sveltejs/kit';
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense';
|
import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense';
|
||||||
import type { OpnsenseWgPeers } from '$lib/opnsense/wg';
|
import type { OpnsenseWgPeers } from '$lib/opnsense/wg';
|
||||||
|
import { findDevices } from '$lib/server/devices';
|
||||||
|
import type { ConnectionDetails } from '$lib/connections';
|
||||||
|
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
if (!event.locals.user) {
|
if (!event.locals.user) {
|
||||||
return error(401, 'Unauthorized');
|
return error(401, 'Unauthorized');
|
||||||
}
|
}
|
||||||
|
console.debug('/api/connections');
|
||||||
|
const peers = await fetchOpnsensePeers(event.locals.user.username);
|
||||||
|
console.debug('/api/connections: fetched opnsense peers', peers.rowCount);
|
||||||
|
const devices = await findDevices(event.locals.user.id);
|
||||||
|
console.debug('/api/connections: fetched db devices');
|
||||||
|
|
||||||
|
if (!peers) {
|
||||||
|
return error(500, 'Error getting info from OPNsense API');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this is all garbage performance
|
||||||
|
// filter devices with no recent handshakes
|
||||||
|
peers.rows = peers.rows.filter((peer) => peer['latest-handshake']);
|
||||||
|
|
||||||
|
// start from devices, to treat db as the source of truth
|
||||||
|
const connections: ConnectionDetails[] = [];
|
||||||
|
for (const device of devices) {
|
||||||
|
const peerData = peers.rows.find((peer) => peer['public-key'] === device.publicKey);
|
||||||
|
if (!peerData) continue;
|
||||||
|
connections.push({
|
||||||
|
deviceId: device.id,
|
||||||
|
deviceName: device.name,
|
||||||
|
devicePublicKey: device.publicKey,
|
||||||
|
deviceIps: peerData['allowed-ips'].split(','),
|
||||||
|
endpoint: peerData['endpoint'],
|
||||||
|
// swap rx and tx, since the opnsense values are from the server perspective
|
||||||
|
transferRx: peerData['transfer-tx'],
|
||||||
|
transferTx: peerData['transfer-rx'],
|
||||||
|
latestHandshake: peerData['latest-handshake'] * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(connections), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'max-age=5',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchOpnsensePeers(username: string) {
|
||||||
const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`;
|
const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`;
|
||||||
const options: RequestInit = {
|
const options: RequestInit = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -16,28 +59,17 @@ export const GET: RequestHandler = async (event) => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
'current': 1,
|
current: 1,
|
||||||
// "rowCount": 7,
|
// "rowCount": 7,
|
||||||
'sort': {},
|
sort: {},
|
||||||
// TODO: use a more unique search phrase
|
// TODO: use a more unique search phrase
|
||||||
// unfortunately 64 character limit,
|
// unfortunately 64 character limit,
|
||||||
// but it should be fine if users can't change their own username
|
// but it should be fine if users can't change their own username
|
||||||
'searchPhrase': `vpgen-${event.locals.user.username}`,
|
searchPhrase: `vpgen-${username}`,
|
||||||
'type': ['peer'],
|
type: ['peer'],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(apiUrl, options);
|
const res = await fetch(apiUrl, options);
|
||||||
const peers = await res.json() as OpnsenseWgPeers;
|
return (await res.json()) as OpnsenseWgPeers;
|
||||||
peers.rows = peers.rows.filter(peer => peer['latest-handshake'])
|
}
|
||||||
|
|
||||||
if (!peers) {
|
|
||||||
return error(500, 'Error getting info from OPNsense API');
|
|
||||||
}
|
|
||||||
return new Response(JSON.stringify(peers), {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Cache-Control': 'max-age=5',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getSize(size: number) {
|
function toSizeString(size: number) {
|
||||||
let sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
let sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
|
||||||
for (let i = 1; i < sizes.length; i++) {
|
for (let i = 1; i < sizes.length; i++) {
|
||||||
@ -34,35 +34,31 @@
|
|||||||
<Table.Root class="divide-y-2 divide-background overflow-hidden rounded-lg bg-accent">
|
<Table.Root class="divide-y-2 divide-background overflow-hidden rounded-lg bg-accent">
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Head scope="col">Name</Table.Head>
|
<Table.Head scope="col">Device</Table.Head>
|
||||||
<Table.Head scope="col">Public Key</Table.Head>
|
<Table.Head scope="col">Public Key</Table.Head>
|
||||||
<Table.Head scope="col">Endpoint</Table.Head>
|
<Table.Head scope="col">Endpoint</Table.Head>
|
||||||
<Table.Head scope="col">Allowed IPs</Table.Head>
|
<Table.Head scope="col">Device IPs</Table.Head>
|
||||||
<Table.Head scope="col">Latest Handshake</Table.Head>
|
<Table.Head scope="col">Latest Handshake</Table.Head>
|
||||||
<Table.Head scope="col">RX</Table.Head>
|
<Table.Head scope="col">RX</Table.Head>
|
||||||
<Table.Head scope="col">TX</Table.Head>
|
<Table.Head scope="col">TX</Table.Head>
|
||||||
<Table.Head scope="col" class="hidden">Persistent Keepalive</Table.Head>
|
|
||||||
<Table.Head scope="col" class="hidden">Interface Name</Table.Head>
|
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body class="divide-y-2 divide-background">
|
<Table.Body class="divide-y-2 divide-background">
|
||||||
{#each data.peers.rows as peer}
|
{#each data.connections as conn}
|
||||||
<Table.Row class="hover:bg-surface">
|
<Table.Row class="hover:bg-surface">
|
||||||
<Table.Head scope="row">{peer.name}</Table.Head>
|
<Table.Head scope="row">{conn.deviceName}</Table.Head>
|
||||||
<Table.Cell class="max-w-[10ch] truncate">{peer['public-key']}</Table.Cell>
|
<Table.Cell class="max-w-[10ch] truncate">{conn.devicePublicKey}</Table.Cell>
|
||||||
<Table.Cell>{peer.endpoint}</Table.Cell>
|
<Table.Cell>{conn.endpoint}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each peer['allowed-ips'].split(',') as addr}
|
{#each conn.deviceIps as addr}
|
||||||
<Badge class="select-auto bg-background" variant="secondary">{addr}</Badge>
|
<Badge class="select-auto bg-background" variant="secondary">{addr}</Badge>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>{new Date(peer['latest-handshake'] * 1000).toLocaleString()}</Table.Cell>
|
<Table.Cell>{new Date(conn.latestHandshake).toLocaleString()}</Table.Cell>
|
||||||
<Table.Cell>{getSize(peer['transfer-rx'])}</Table.Cell>
|
<Table.Cell>{toSizeString(conn.transferRx)}</Table.Cell>
|
||||||
<Table.Cell>{getSize(peer['transfer-tx'])}</Table.Cell>
|
<Table.Cell>{toSizeString(conn.transferTx)}</Table.Cell>
|
||||||
<Table.Cell class="hidden">{peer['persistent-keepalive']}</Table.Cell>
|
|
||||||
<Table.Cell class="hidden">{peer.ifname}</Table.Cell>
|
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
{/each}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
import type { OpnsenseWgPeers } from '$lib/opnsense/wg';
|
import type { ConnectionDetails } from '$lib/connections';
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
const res = await fetch('/api/connections');
|
const res = await fetch('/api/connections');
|
||||||
const peers = await res.json() as OpnsenseWgPeers;
|
const connections = await res.json() as ConnectionDetails[];
|
||||||
|
|
||||||
return { peers };
|
return { connections };
|
||||||
};
|
};
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
<Table.Row class="hover:bg-surface group">
|
<Table.Row class="hover:bg-surface group">
|
||||||
<Table.Head scope="row">
|
<Table.Head scope="row">
|
||||||
<a
|
<a
|
||||||
href={`/devices/${device.id}`}
|
href="/devices/{device.id}"
|
||||||
class="flex size-full items-center group-hover:underline"
|
class="flex size-full items-center group-hover:underline"
|
||||||
>
|
>
|
||||||
{device.name}
|
{device.name}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user