Compare commits
3 Commits
32927dfd55
...
76b5d9bf97
Author | SHA1 | Date | |
---|---|---|---|
76b5d9bf97 | |||
85573f5791 | |||
03fb89dc8b |
20
.idea/dataSources.xml
generated
Normal file
20
.idea/dataSources.xml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="local" uuid="f362368b-ba47-4270-9f81-b0d8484b9928">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/local.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
<libraries>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
|
||||
</library>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
32
drizzle/0000_young_wong.sql
Normal file
32
drizzle/0000_young_wong.sql
Normal file
@ -0,0 +1,32 @@
|
||||
CREATE TABLE `ip_allocations` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`client_id` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `wg_clients`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `ip_allocations_client_id_unique` ON `ip_allocations` (`client_id`);--> statement-breakpoint
|
||||
CREATE TABLE `sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`username` text NOT NULL,
|
||||
`name` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `wg_clients` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`opnsense_id` text,
|
||||
`public_key` text NOT NULL,
|
||||
`private_key` text,
|
||||
`pre_shared_key` text,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `wg_clients_public_key_unique` ON `wg_clients` (`public_key`);
|
221
drizzle/meta/0000_snapshot.json
Normal file
221
drizzle/meta/0000_snapshot.json
Normal file
@ -0,0 +1,221 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "29e6fd88-fa47-4f79-ad83-c52538bc36a6",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"ip_allocations": {
|
||||
"name": "ip_allocations",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"client_id": {
|
||||
"name": "client_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"ip_allocations_client_id_unique": {
|
||||
"name": "ip_allocations_client_id_unique",
|
||||
"columns": [
|
||||
"client_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"ip_allocations_client_id_wg_clients_id_fk": {
|
||||
"name": "ip_allocations_client_id_wg_clients_id_fk",
|
||||
"tableFrom": "ip_allocations",
|
||||
"tableTo": "wg_clients",
|
||||
"columnsFrom": [
|
||||
"client_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_user_id_users_id_fk": {
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"wg_clients": {
|
||||
"name": "wg_clients",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"opnsense_id": {
|
||||
"name": "opnsense_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"private_key": {
|
||||
"name": "private_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pre_shared_key": {
|
||||
"name": "pre_shared_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"wg_clients_public_key_unique": {
|
||||
"name": "wg_clients_public_key_unique",
|
||||
"columns": [
|
||||
"public_key"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"wg_clients_user_id_users_id_fk": {
|
||||
"name": "wg_clients_user_id_users_id_fk",
|
||||
"tableFrom": "wg_clients",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1735028333867,
|
||||
"tag": "0000_young_wong",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -25,6 +25,7 @@
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "^0.21.16",
|
||||
"clsx": "^2.1.1",
|
||||
@ -52,6 +53,7 @@
|
||||
"arctic": "^2.2.1",
|
||||
"drizzle-orm": "^0.38.2",
|
||||
"ip-address": "^10.0.1",
|
||||
"lucide-svelte": "^0.454.0"
|
||||
"lucide-svelte": "^0.454.0",
|
||||
"qrcode-svg": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
24
src/app.css
24
src/app.css
@ -4,16 +4,16 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background: 0 0% 90%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover: 0 0% 90%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card: 0 0% 90%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
@ -22,10 +22,10 @@
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary: 210 26% 86%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent: 210 26% 86%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
@ -39,31 +39,31 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--foreground: 210 40% 90%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--popover-foreground: 210 40% 90%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--card-foreground: 210 40% 90%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary: 210 40% 90%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--secondary-foreground: 210 40% 90%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent-foreground: 210 40% 90%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive-foreground: 210 40% 90%;
|
||||
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
|
@ -31,14 +31,17 @@ const handleAuth: Handle = async ({ event, resolve }) => {
|
||||
};
|
||||
|
||||
|
||||
const authRequired = new Set([
|
||||
'/user',
|
||||
'/connections',
|
||||
'/api/connections',
|
||||
]);
|
||||
const authRequired = [
|
||||
/^\/api/,
|
||||
/^\/user/,
|
||||
/^\/connections/,
|
||||
/^\/clients/,
|
||||
];
|
||||
const handleProtectedPaths: Handle = ({ event, resolve }) => {
|
||||
if (authRequired.has(event.url.pathname) && !event.locals.user) {
|
||||
return redirect(302, '/');
|
||||
const isProtected = authRequired.some((re) => re.test(event.url.pathname));
|
||||
|
||||
if (!event.locals.user && isProtected) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
return resolve(event);
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ export async function createClient(params: {
|
||||
username: params.user.username,
|
||||
pubkey: keys.pubkey,
|
||||
psk: keys.psk,
|
||||
allowedIps: getIpsFromIndex(ipAllocationId - 1).join(','),
|
||||
allowedIps: getIpsFromIndex(ipAllocationId).join(','),
|
||||
});
|
||||
const opnsenseResJson = await opnsenseRes.json();
|
||||
if (opnsenseResJson['result'] !== 'saved') {
|
||||
@ -178,6 +178,7 @@ async function getKeys() {
|
||||
}
|
||||
|
||||
export function getIpsFromIndex(ipIndex: number) {
|
||||
ipIndex -= 1; // 1-indexed in the db
|
||||
const v4StartingAddr = new Address4(IPV4_STARTING_ADDR);
|
||||
const v6StartingAddr = new Address6(IPV6_STARTING_ADDR);
|
||||
const v4Allowed = Address4.fromBigInt(v4StartingAddr.bigInt() + BigInt(ipIndex));
|
||||
|
@ -26,7 +26,7 @@ export const ipAllocations = sqliteTable('ip_allocations', {
|
||||
// unique for now, only allowing one allocation per client
|
||||
clientId: integer('client_id')
|
||||
.unique()
|
||||
.references(() => wgClients.id),
|
||||
.references(() => wgClients.id, { onDelete: 'set null' }),
|
||||
});
|
||||
|
||||
export const wgClients = sqliteTable('wg_clients', {
|
||||
|
@ -6,20 +6,20 @@
|
||||
const { data, children } = $props();
|
||||
const { user } = data;
|
||||
|
||||
function getNavClass(path: string) {
|
||||
function getNavClass(path: RegExp) {
|
||||
return cn('hover:text-foreground/80 transition-colors',
|
||||
$page.url.pathname.startsWith(path) ? 'text-foreground' : 'text-foreground/60');
|
||||
path.test($page.url.pathname) ? 'text-foreground' : 'text-foreground/60');
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="p-4 sm:flex">
|
||||
<span class=" mr-6 font-bold sm:inline-block">VPGen</span>
|
||||
<nav class="flex items-center gap-6 text-sm">
|
||||
<a href="/" class={getNavClass("/")}>Home</a>
|
||||
<a href="/" class={getNavClass(/^\/$/)}>Home</a>
|
||||
{#if user}
|
||||
<a href="/user" class={getNavClass("/user")}>Profile</a>
|
||||
<a href="/connections" class={getNavClass("/connections")}>Connections</a>
|
||||
<a href="/clients" class={getNavClass("/clients")}>Clients</a>
|
||||
<a href="/user" class={getNavClass(/^\/user$/)}>Profile</a>
|
||||
<a href="/connections" class={getNavClass(/^\/connections$/)}>Connections</a>
|
||||
<a href="/clients" class={getNavClass(/^\/clients(\/\d+)?$/)}>Clients</a>
|
||||
{/if}
|
||||
</nav>
|
||||
</header>
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { wgClients } from '$lib/server/db/schema';
|
||||
import { db } from '$lib/server/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { createClient, getIpsFromIndex } from '$lib/server/clients';
|
||||
import { createClient, findClients, mapClientToDetails } from '$lib/server/clients';
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
@ -13,38 +10,11 @@ export const GET: RequestHandler = async (event) => {
|
||||
const clients = await findClients(event.locals.user.id);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
clients,
|
||||
clients: clients.map(mapClientToDetails),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
async function findClients(userId: string) {
|
||||
const clientsData = await db.query.wgClients.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
publicKey: true,
|
||||
privateKey: true,
|
||||
preSharedKey: true,
|
||||
},
|
||||
with: {
|
||||
ipAllocation: true,
|
||||
},
|
||||
where: eq(wgClients.userId, userId),
|
||||
});
|
||||
// replace ip index with actual addresses
|
||||
return clientsData.map((client) => {
|
||||
const ips = getIpsFromIndex(client.ipAllocation.id);
|
||||
return {
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
publicKey: client.publicKey,
|
||||
privateKey: client.privateKey,
|
||||
preSharedKey: client.preSharedKey,
|
||||
ips,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export type Clients = Awaited<ReturnType<typeof findClients>>;
|
||||
|
||||
|
@ -5,9 +5,11 @@ import { error } from '@sveltejs/kit';
|
||||
export const actions = {
|
||||
create: async (event) => {
|
||||
if (!event.locals.user) return error(401, 'Unauthorized');
|
||||
const name = 'New Client Name';
|
||||
const formData = await event.request.formData();
|
||||
const name = formData.get('name');
|
||||
if (typeof name !== 'string' || name.trim() === '') return error(400, 'Invalid name');
|
||||
const res = await createClient({
|
||||
name,
|
||||
name: name.trim(),
|
||||
user: event.locals.user,
|
||||
});
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import type { PageData } from './$types';
|
||||
import { LucidePlus } from 'lucide-svelte';
|
||||
|
||||
@ -12,26 +13,28 @@
|
||||
<title>Clients</title>
|
||||
</svelte:head>
|
||||
|
||||
<Table.Root class="bg-accent rounded-xl">
|
||||
<Table.Root class="bg-accent rounded-lg overflow-hidden divide-y-2 divide-background">
|
||||
<Table.Header>
|
||||
<Table.Head>Name</Table.Head>
|
||||
<Table.Head>Public Key</Table.Head>
|
||||
<Table.Head>Private Key</Table.Head>
|
||||
<Table.Head>Pre-Shared Key</Table.Head>
|
||||
<Table.Head>IP Allocation</Table.Head>
|
||||
<Table.Row>
|
||||
<Table.Head scope="col">Name</Table.Head>
|
||||
<Table.Head scope="col">Public Key</Table.Head>
|
||||
<!-- <Table.Head scope="col">Private Key</Table.Head>-->
|
||||
<!-- <Table.Head scope="col">Pre-Shared Key</Table.Head>-->
|
||||
<Table.Head scope="col">IP Allocation</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Body class="divide-y-2 divide-background">
|
||||
{#each data.clients as client}
|
||||
<Table.Row class="border-y-2 border-background hover:bg-muted-foreground">
|
||||
<a href={`/clients/${client.id}`} class="contents">
|
||||
<Table.Cell>
|
||||
<Table.Row class="hover:bg-background hover:bg-opacity-40 group">
|
||||
<Table.Head scope="row">
|
||||
<a href={`/clients/${client.id}`} class="flex items-center size-full group-hover:underline">
|
||||
{client.name}
|
||||
</Table.Cell>
|
||||
</a>
|
||||
<Table.Cell class="truncate max-w-[10ch]">{client.publicKey}</Table.Cell>
|
||||
<Table.Cell class="truncate max-w-[10ch]">{client.privateKey}</Table.Cell>
|
||||
<Table.Cell class="truncate max-w-[10ch]">{client.preSharedKey}</Table.Cell>
|
||||
<Table.Cell class="flex gap-1">
|
||||
</a>
|
||||
</Table.Head>
|
||||
<Table.Cell class="truncate">{client.publicKey}</Table.Cell>
|
||||
<!-- <Table.Cell class="truncate max-w-[10ch]">{client.privateKey}</Table.Cell>-->
|
||||
<!-- <Table.Cell class="truncate max-w-[10ch]">{client.preSharedKey}</Table.Cell>-->
|
||||
<Table.Cell class="flex flex-wrap gap-1">
|
||||
{#each client.ips as ip}
|
||||
<Badge class="bg-background select-auto" variant="secondary">{ip}</Badge>
|
||||
{/each}
|
||||
@ -42,9 +45,11 @@
|
||||
</Table.Root>
|
||||
|
||||
<!--Floating action button for adding a new client-->
|
||||
<form class="self-end mt-auto pt-4" method="post" action="?/create">
|
||||
<!--Not sure if this is the best place for the input field, will think about it later-->
|
||||
<form class="flex self-end mt-auto pt-4" method="post" action="?/create">
|
||||
<Input type="text" name="name" placeholder="New Client" class="mr-2" />
|
||||
<Button type="submit">
|
||||
<LucidePlus />
|
||||
<LucidePlus class="mr-2 h-4 w-4" />
|
||||
Add Client
|
||||
</Button>
|
||||
</form>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import type { Clients } from '../api/clients/+server';
|
||||
import type { ClientDetails } from '$lib/types/clients';
|
||||
|
||||
export const load: PageLoad = async ({ fetch }) => {
|
||||
const res = await fetch('/api/clients');
|
||||
const { clients } = await res.json() as { clients: Clients };
|
||||
const { clients } = await res.json() as { clients: ClientDetails[] };
|
||||
|
||||
return { clients };
|
||||
};
|
||||
|
@ -1,19 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { LucideClipboardCopy } from 'lucide-svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import QRCode from 'qrcode-svg';
|
||||
|
||||
const { data }: { data: PageData } = $props();
|
||||
|
||||
let tooltipText = $state('Copy to clipboard');
|
||||
let qrCode = new QRCode({
|
||||
content: data.config,
|
||||
join: true,
|
||||
});
|
||||
|
||||
function copyToClipboard() {
|
||||
navigator.clipboard.writeText(data.config).then(() => {
|
||||
tooltipText = 'Copied';
|
||||
});
|
||||
async function copyToClipboard() {
|
||||
await navigator.clipboard.writeText(data.config);
|
||||
tooltipText = 'Copied!';
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
tooltipText = 'Copy to Clipboard';
|
||||
tooltipText = 'Copy to clipboard';
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -21,21 +26,30 @@
|
||||
<title></title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Client: {data.client.name}</h1>
|
||||
<h1 class="bg-accent text-lg w-fit rounded-lg p-2 mb-4">{data.client.name}</h1>
|
||||
|
||||
<div class="flex relative bg-accent p-2 rounded-xl overflow-x-scroll">
|
||||
<pre><code>{data.config}</code></pre>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="relative bg-accent rounded-lg max-w-fit">
|
||||
<div class="flex items-start p-2 overflow-x-auto">
|
||||
<pre><code>{data.config}</code></pre>
|
||||
|
||||
<!--Copy button for the configuration-->
|
||||
<div class="absolute flex right-2 items-center group">
|
||||
<span class="hidden group-hover:block bg-background text-xs rounded py-1 px-2">
|
||||
{tooltipText}
|
||||
</span>
|
||||
<button class="flex items-center justify-center w-10 h-10 bg-background rounded-xl ml-2"
|
||||
onclick={copyToClipboard}
|
||||
onmouseleave="{onMouseLeave}"
|
||||
>
|
||||
<LucideClipboardCopy />
|
||||
</button>
|
||||
<!--Copy button for the configuration-->
|
||||
<!--Flex reverse for peer hover to work properly-->
|
||||
<div class="absolute group flex flex-row-reverse items-center gap-1 right-2">
|
||||
<Button class="peer size-10 p-2"
|
||||
onclick={copyToClipboard}
|
||||
onmouseleave={onMouseLeave}
|
||||
>
|
||||
<LucideClipboardCopy />
|
||||
</Button>
|
||||
<span class="hidden peer-hover:block bg-background text-xs rounded-lg p-2">
|
||||
{tooltipText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg overflow-hidden">
|
||||
{@html qrCode.svg()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,15 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import type { ClientDetails } from '$lib/types/clients';
|
||||
import { clientDetailsToConfig } from '$lib/clients';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const res = await fetch(`/api/clients/${params.id}`);
|
||||
const client = (await res.json()) as ClientDetails;
|
||||
const resJson = await res.json();
|
||||
if (!res.ok) {
|
||||
return error(res.status, resJson['message']);
|
||||
}
|
||||
const client = resJson as ClientDetails;
|
||||
const config = clientDetailsToConfig(client);
|
||||
|
||||
return { client, config };
|
||||
|
@ -33,22 +33,24 @@
|
||||
<title>Connections</title>
|
||||
</svelte:head>
|
||||
|
||||
<Table.Root class="bg-accent rounded-xl">
|
||||
<Table.Root class="bg-accent rounded-lg overflow-hidden divide-y-2 divide-background">
|
||||
<Table.Header>
|
||||
<Table.Head>Name</Table.Head>
|
||||
<Table.Head>Public Key</Table.Head>
|
||||
<Table.Head>Endpoint</Table.Head>
|
||||
<Table.Head>Allowed IPs</Table.Head>
|
||||
<Table.Head>Latest Handshake</Table.Head>
|
||||
<Table.Head>RX</Table.Head>
|
||||
<Table.Head>TX</Table.Head>
|
||||
<Table.Head class="hidden">Persistent Keepalive</Table.Head>
|
||||
<Table.Head class="hidden">Interface Name</Table.Head>
|
||||
<Table.Row>
|
||||
<Table.Head scope="col">Name</Table.Head>
|
||||
<Table.Head scope="col">Public Key</Table.Head>
|
||||
<Table.Head scope="col">Endpoint</Table.Head>
|
||||
<Table.Head scope="col">Allowed IPs</Table.Head>
|
||||
<Table.Head scope="col">Latest Handshake</Table.Head>
|
||||
<Table.Head scope="col">RX</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.Header>
|
||||
<Table.Body>
|
||||
<Table.Body class="divide-y-2 divide-background">
|
||||
{#each data.peers.rows as peer}
|
||||
<Table.Row class="border-y-2 border-background">
|
||||
<Table.Cell>{peer.name}</Table.Cell>
|
||||
<Table.Row class="hover:bg-background hover:bg-opacity-40">
|
||||
<Table.Head scope="row">{peer.name}</Table.Head>
|
||||
<Table.Cell class="truncate max-w-[10ch]">{peer['public-key']}</Table.Cell>
|
||||
<Table.Cell>{peer.endpoint}</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
Loading…
x
Reference in New Issue
Block a user