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",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/better-sqlite3": "^7.6.11",
|
"@types/better-sqlite3": "^7.6.11",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
|
"@types/qrcode-svg": "^1.1.5",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"bits-ui": "^0.21.16",
|
"bits-ui": "^0.21.16",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -52,6 +53,7 @@
|
|||||||
"arctic": "^2.2.1",
|
"arctic": "^2.2.1",
|
||||||
"drizzle-orm": "^0.38.2",
|
"drizzle-orm": "^0.38.2",
|
||||||
"ip-address": "^10.0.1",
|
"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 {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 90%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 96.1%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 90%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 90%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 31.8% 91.4%;
|
||||||
@ -22,10 +22,10 @@
|
|||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 222.2 47.4% 11.2%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 26% 86%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--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%;
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 72.2% 50.6%;
|
||||||
@ -39,31 +39,31 @@
|
|||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 40% 90%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 217.2 32.6% 17.5%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 90%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 222.2 84% 4.9%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 210 40% 90%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 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%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--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: 217.2 32.6% 17.5%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 90%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 90%;
|
||||||
|
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
}
|
}
|
||||||
|
@ -31,14 +31,17 @@ const handleAuth: Handle = async ({ event, resolve }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const authRequired = new Set([
|
const authRequired = [
|
||||||
'/user',
|
/^\/api/,
|
||||||
'/connections',
|
/^\/user/,
|
||||||
'/api/connections',
|
/^\/connections/,
|
||||||
]);
|
/^\/clients/,
|
||||||
|
];
|
||||||
const handleProtectedPaths: Handle = ({ event, resolve }) => {
|
const handleProtectedPaths: Handle = ({ event, resolve }) => {
|
||||||
if (authRequired.has(event.url.pathname) && !event.locals.user) {
|
const isProtected = authRequired.some((re) => re.test(event.url.pathname));
|
||||||
return redirect(302, '/');
|
|
||||||
|
if (!event.locals.user && isProtected) {
|
||||||
|
return redirect(302, '/');
|
||||||
}
|
}
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ export async function createClient(params: {
|
|||||||
username: params.user.username,
|
username: params.user.username,
|
||||||
pubkey: keys.pubkey,
|
pubkey: keys.pubkey,
|
||||||
psk: keys.psk,
|
psk: keys.psk,
|
||||||
allowedIps: getIpsFromIndex(ipAllocationId - 1).join(','),
|
allowedIps: getIpsFromIndex(ipAllocationId).join(','),
|
||||||
});
|
});
|
||||||
const opnsenseResJson = await opnsenseRes.json();
|
const opnsenseResJson = await opnsenseRes.json();
|
||||||
if (opnsenseResJson['result'] !== 'saved') {
|
if (opnsenseResJson['result'] !== 'saved') {
|
||||||
@ -178,6 +178,7 @@ async function getKeys() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getIpsFromIndex(ipIndex: number) {
|
export function getIpsFromIndex(ipIndex: number) {
|
||||||
|
ipIndex -= 1; // 1-indexed in the db
|
||||||
const v4StartingAddr = new Address4(IPV4_STARTING_ADDR);
|
const v4StartingAddr = new Address4(IPV4_STARTING_ADDR);
|
||||||
const v6StartingAddr = new Address6(IPV6_STARTING_ADDR);
|
const v6StartingAddr = new Address6(IPV6_STARTING_ADDR);
|
||||||
const v4Allowed = Address4.fromBigInt(v4StartingAddr.bigInt() + BigInt(ipIndex));
|
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
|
// unique for now, only allowing one allocation per client
|
||||||
clientId: integer('client_id')
|
clientId: integer('client_id')
|
||||||
.unique()
|
.unique()
|
||||||
.references(() => wgClients.id),
|
.references(() => wgClients.id, { onDelete: 'set null' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const wgClients = sqliteTable('wg_clients', {
|
export const wgClients = sqliteTable('wg_clients', {
|
||||||
|
@ -6,20 +6,20 @@
|
|||||||
const { data, children } = $props();
|
const { data, children } = $props();
|
||||||
const { user } = data;
|
const { user } = data;
|
||||||
|
|
||||||
function getNavClass(path: string) {
|
function getNavClass(path: RegExp) {
|
||||||
return cn('hover:text-foreground/80 transition-colors',
|
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>
|
</script>
|
||||||
|
|
||||||
<header class="p-4 sm:flex">
|
<header class="p-4 sm:flex">
|
||||||
<span class=" mr-6 font-bold sm:inline-block">VPGen</span>
|
<span class=" mr-6 font-bold sm:inline-block">VPGen</span>
|
||||||
<nav class="flex items-center gap-6 text-sm">
|
<nav class="flex items-center gap-6 text-sm">
|
||||||
<a href="/" class={getNavClass("/")}>Home</a>
|
<a href="/" class={getNavClass(/^\/$/)}>Home</a>
|
||||||
{#if user}
|
{#if user}
|
||||||
<a href="/user" class={getNavClass("/user")}>Profile</a>
|
<a href="/user" class={getNavClass(/^\/user$/)}>Profile</a>
|
||||||
<a href="/connections" class={getNavClass("/connections")}>Connections</a>
|
<a href="/connections" class={getNavClass(/^\/connections$/)}>Connections</a>
|
||||||
<a href="/clients" class={getNavClass("/clients")}>Clients</a>
|
<a href="/clients" class={getNavClass(/^\/clients(\/\d+)?$/)}>Clients</a>
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
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 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) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
if (!event.locals.user) {
|
if (!event.locals.user) {
|
||||||
@ -13,38 +10,11 @@ export const GET: RequestHandler = async (event) => {
|
|||||||
const clients = await findClients(event.locals.user.id);
|
const clients = await findClients(event.locals.user.id);
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
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>>;
|
export type Clients = Awaited<ReturnType<typeof findClients>>;
|
||||||
|
|
||||||
|
@ -5,9 +5,11 @@ import { error } from '@sveltejs/kit';
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
create: async (event) => {
|
create: async (event) => {
|
||||||
if (!event.locals.user) return error(401, 'Unauthorized');
|
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({
|
const res = await createClient({
|
||||||
name,
|
name: name.trim(),
|
||||||
user: event.locals.user,
|
user: event.locals.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { LucidePlus } from 'lucide-svelte';
|
import { LucidePlus } from 'lucide-svelte';
|
||||||
|
|
||||||
@ -12,26 +13,28 @@
|
|||||||
<title>Clients</title>
|
<title>Clients</title>
|
||||||
</svelte:head>
|
</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.Header>
|
||||||
<Table.Head>Name</Table.Head>
|
<Table.Row>
|
||||||
<Table.Head>Public Key</Table.Head>
|
<Table.Head scope="col">Name</Table.Head>
|
||||||
<Table.Head>Private Key</Table.Head>
|
<Table.Head scope="col">Public Key</Table.Head>
|
||||||
<Table.Head>Pre-Shared Key</Table.Head>
|
<!-- <Table.Head scope="col">Private Key</Table.Head>-->
|
||||||
<Table.Head>IP Allocation</Table.Head>
|
<!-- <Table.Head scope="col">Pre-Shared Key</Table.Head>-->
|
||||||
|
<Table.Head scope="col">IP Allocation</Table.Head>
|
||||||
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body class="divide-y-2 divide-background">
|
||||||
{#each data.clients as client}
|
{#each data.clients as client}
|
||||||
<Table.Row class="border-y-2 border-background hover:bg-muted-foreground">
|
<Table.Row class="hover:bg-background hover:bg-opacity-40 group">
|
||||||
<a href={`/clients/${client.id}`} class="contents">
|
<Table.Head scope="row">
|
||||||
<Table.Cell>
|
<a href={`/clients/${client.id}`} class="flex items-center size-full group-hover:underline">
|
||||||
{client.name}
|
{client.name}
|
||||||
</Table.Cell>
|
</a>
|
||||||
</a>
|
</Table.Head>
|
||||||
<Table.Cell class="truncate max-w-[10ch]">{client.publicKey}</Table.Cell>
|
<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.privateKey}</Table.Cell>-->
|
||||||
<Table.Cell class="truncate max-w-[10ch]">{client.preSharedKey}</Table.Cell>
|
<!-- <Table.Cell class="truncate max-w-[10ch]">{client.preSharedKey}</Table.Cell>-->
|
||||||
<Table.Cell class="flex gap-1">
|
<Table.Cell class="flex flex-wrap gap-1">
|
||||||
{#each client.ips as ip}
|
{#each client.ips as ip}
|
||||||
<Badge class="bg-background select-auto" variant="secondary">{ip}</Badge>
|
<Badge class="bg-background select-auto" variant="secondary">{ip}</Badge>
|
||||||
{/each}
|
{/each}
|
||||||
@ -42,9 +45,11 @@
|
|||||||
</Table.Root>
|
</Table.Root>
|
||||||
|
|
||||||
<!--Floating action button for adding a new client-->
|
<!--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">
|
<Button type="submit">
|
||||||
<LucidePlus />
|
<LucidePlus class="mr-2 h-4 w-4" />
|
||||||
Add Client
|
Add Client
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { PageLoad } from './$types';
|
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 }) => {
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
const res = await fetch('/api/clients');
|
const res = await fetch('/api/clients');
|
||||||
const { clients } = await res.json() as { clients: Clients };
|
const { clients } = await res.json() as { clients: ClientDetails[] };
|
||||||
|
|
||||||
return { clients };
|
return { clients };
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { LucideClipboardCopy } from 'lucide-svelte';
|
import { LucideClipboardCopy } from 'lucide-svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import QRCode from 'qrcode-svg';
|
||||||
|
|
||||||
const { data }: { data: PageData } = $props();
|
const { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
let tooltipText = $state('Copy to clipboard');
|
let tooltipText = $state('Copy to clipboard');
|
||||||
|
let qrCode = new QRCode({
|
||||||
|
content: data.config,
|
||||||
|
join: true,
|
||||||
|
});
|
||||||
|
|
||||||
function copyToClipboard() {
|
async function copyToClipboard() {
|
||||||
navigator.clipboard.writeText(data.config).then(() => {
|
await navigator.clipboard.writeText(data.config);
|
||||||
tooltipText = 'Copied';
|
tooltipText = 'Copied!';
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
tooltipText = 'Copy to Clipboard';
|
tooltipText = 'Copy to clipboard';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -21,21 +26,30 @@
|
|||||||
<title></title>
|
<title></title>
|
||||||
</svelte:head>
|
</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">
|
<div class="flex flex-wrap gap-4">
|
||||||
<pre><code>{data.config}</code></pre>
|
<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-->
|
<!--Copy button for the configuration-->
|
||||||
<div class="absolute flex right-2 items-center group">
|
<!--Flex reverse for peer hover to work properly-->
|
||||||
<span class="hidden group-hover:block bg-background text-xs rounded py-1 px-2">
|
<div class="absolute group flex flex-row-reverse items-center gap-1 right-2">
|
||||||
{tooltipText}
|
<Button class="peer size-10 p-2"
|
||||||
</span>
|
onclick={copyToClipboard}
|
||||||
<button class="flex items-center justify-center w-10 h-10 bg-background rounded-xl ml-2"
|
onmouseleave={onMouseLeave}
|
||||||
onclick={copyToClipboard}
|
>
|
||||||
onmouseleave="{onMouseLeave}"
|
<LucideClipboardCopy />
|
||||||
>
|
</Button>
|
||||||
<LucideClipboardCopy />
|
<span class="hidden peer-hover:block bg-background text-xs rounded-lg p-2">
|
||||||
</button>
|
{tooltipText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</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 { PageLoad } from './$types';
|
||||||
import type { ClientDetails } from '$lib/types/clients';
|
import type { ClientDetails } from '$lib/types/clients';
|
||||||
import { clientDetailsToConfig } from '$lib/clients';
|
import { clientDetailsToConfig } from '$lib/clients';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
const res = await fetch(`/api/clients/${params.id}`);
|
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);
|
const config = clientDetailsToConfig(client);
|
||||||
|
|
||||||
return { client, config };
|
return { client, config };
|
||||||
|
@ -33,22 +33,24 @@
|
|||||||
<title>Connections</title>
|
<title>Connections</title>
|
||||||
</svelte:head>
|
</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.Header>
|
||||||
<Table.Head>Name</Table.Head>
|
<Table.Row>
|
||||||
<Table.Head>Public Key</Table.Head>
|
<Table.Head scope="col">Name</Table.Head>
|
||||||
<Table.Head>Endpoint</Table.Head>
|
<Table.Head scope="col">Public Key</Table.Head>
|
||||||
<Table.Head>Allowed IPs</Table.Head>
|
<Table.Head scope="col">Endpoint</Table.Head>
|
||||||
<Table.Head>Latest Handshake</Table.Head>
|
<Table.Head scope="col">Allowed IPs</Table.Head>
|
||||||
<Table.Head>RX</Table.Head>
|
<Table.Head scope="col">Latest Handshake</Table.Head>
|
||||||
<Table.Head>TX</Table.Head>
|
<Table.Head scope="col">RX</Table.Head>
|
||||||
<Table.Head class="hidden">Persistent Keepalive</Table.Head>
|
<Table.Head scope="col">TX</Table.Head>
|
||||||
<Table.Head class="hidden">Interface Name</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.Header>
|
||||||
<Table.Body>
|
<Table.Body class="divide-y-2 divide-background">
|
||||||
{#each data.peers.rows as peer}
|
{#each data.peers.rows as peer}
|
||||||
<Table.Row class="border-y-2 border-background">
|
<Table.Row class="hover:bg-background hover:bg-opacity-40">
|
||||||
<Table.Cell>{peer.name}</Table.Cell>
|
<Table.Head scope="row">{peer.name}</Table.Head>
|
||||||
<Table.Cell class="truncate max-w-[10ch]">{peer['public-key']}</Table.Cell>
|
<Table.Cell class="truncate max-w-[10ch]">{peer['public-key']}</Table.Cell>
|
||||||
<Table.Cell>{peer.endpoint}</Table.Cell>
|
<Table.Cell>{peer.endpoint}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user