Compare commits
No commits in common. "76b5d9bf979b4d5939f9455aea40eab34ab20361" and "32927dfd55e8bd44620671db933f97d205391dee" have entirely different histories.
76b5d9bf97
...
32927dfd55
20
.idea/dataSources.xml
generated
20
.idea/dataSources.xml
generated
@ -1,20 +0,0 @@
|
|||||||
<?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>
|
|
@ -1,32 +0,0 @@
|
|||||||
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`);
|
|
@ -1,221 +0,0 @@
|
|||||||
{
|
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1735028333867,
|
|
||||||
"tag": "0000_young_wong",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -25,7 +25,6 @@
|
|||||||
"@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",
|
||||||
@ -53,7 +52,6 @@
|
|||||||
"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% 90%;
|
--background: 0 0% 100%;
|
||||||
--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% 90%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--card: 0 0% 90%;
|
--card: 0 0% 100%;
|
||||||
--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 26% 86%;
|
--secondary: 210 40% 96.1%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--accent: 210 26% 86%;
|
--accent: 210 40% 96.1%;
|
||||||
--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% 90%;
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
--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% 90%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 222.2 84% 4.9%;
|
||||||
--card-foreground: 210 40% 90%;
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--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% 90%;
|
--primary: 210 40% 98%;
|
||||||
--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% 90%;
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 217.2 32.6% 17.5%;
|
||||||
--accent-foreground: 210 40% 90%;
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 90%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
}
|
}
|
||||||
|
@ -31,17 +31,14 @@ const handleAuth: Handle = async ({ event, resolve }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const authRequired = [
|
const authRequired = new Set([
|
||||||
/^\/api/,
|
'/user',
|
||||||
/^\/user/,
|
'/connections',
|
||||||
/^\/connections/,
|
'/api/connections',
|
||||||
/^\/clients/,
|
]);
|
||||||
];
|
|
||||||
const handleProtectedPaths: Handle = ({ event, resolve }) => {
|
const handleProtectedPaths: Handle = ({ event, resolve }) => {
|
||||||
const isProtected = authRequired.some((re) => re.test(event.url.pathname));
|
if (authRequired.has(event.url.pathname) && !event.locals.user) {
|
||||||
|
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).join(','),
|
allowedIps: getIpsFromIndex(ipAllocationId - 1).join(','),
|
||||||
});
|
});
|
||||||
const opnsenseResJson = await opnsenseRes.json();
|
const opnsenseResJson = await opnsenseRes.json();
|
||||||
if (opnsenseResJson['result'] !== 'saved') {
|
if (opnsenseResJson['result'] !== 'saved') {
|
||||||
@ -178,7 +178,6 @@ 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, { onDelete: 'set null' }),
|
.references(() => wgClients.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
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: RegExp) {
|
function getNavClass(path: string) {
|
||||||
return cn('hover:text-foreground/80 transition-colors',
|
return cn('hover:text-foreground/80 transition-colors',
|
||||||
path.test($page.url.pathname) ? 'text-foreground' : 'text-foreground/60');
|
$page.url.pathname.startsWith(path) ? '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(\/\d+)?$/)}>Clients</a>
|
<a href="/clients" class={getNavClass("/clients")}>Clients</a>
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
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, findClients, mapClientToDetails } from '$lib/server/clients';
|
import { createClient, getIpsFromIndex } from '$lib/server/clients';
|
||||||
|
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
if (!event.locals.user) {
|
if (!event.locals.user) {
|
||||||
@ -10,11 +13,38 @@ 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.map(mapClientToDetails),
|
clients,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,11 +5,9 @@ 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 formData = await event.request.formData();
|
const name = 'New Client Name';
|
||||||
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.trim(),
|
name,
|
||||||
user: event.locals.user,
|
user: event.locals.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
@ -13,28 +12,26 @@
|
|||||||
<title>Clients</title>
|
<title>Clients</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Table.Root class="bg-accent rounded-lg overflow-hidden divide-y-2 divide-background">
|
<Table.Root class="bg-accent rounded-xl">
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Head>Name</Table.Head>
|
||||||
<Table.Head scope="col">Name</Table.Head>
|
<Table.Head>Public Key</Table.Head>
|
||||||
<Table.Head scope="col">Public Key</Table.Head>
|
<Table.Head>Private Key</Table.Head>
|
||||||
<!-- <Table.Head scope="col">Private Key</Table.Head>-->
|
<Table.Head>Pre-Shared Key</Table.Head>
|
||||||
<!-- <Table.Head scope="col">Pre-Shared Key</Table.Head>-->
|
<Table.Head>IP Allocation</Table.Head>
|
||||||
<Table.Head scope="col">IP Allocation</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body class="divide-y-2 divide-background">
|
<Table.Body>
|
||||||
{#each data.clients as client}
|
{#each data.clients as client}
|
||||||
<Table.Row class="hover:bg-background hover:bg-opacity-40 group">
|
<Table.Row class="border-y-2 border-background hover:bg-muted-foreground">
|
||||||
<Table.Head scope="row">
|
<a href={`/clients/${client.id}`} class="contents">
|
||||||
<a href={`/clients/${client.id}`} class="flex items-center size-full group-hover:underline">
|
<Table.Cell>
|
||||||
{client.name}
|
{client.name}
|
||||||
</a>
|
</Table.Cell>
|
||||||
</Table.Head>
|
</a>
|
||||||
<Table.Cell class="truncate">{client.publicKey}</Table.Cell>
|
<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.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 flex-wrap gap-1">
|
<Table.Cell class="flex 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}
|
||||||
@ -45,11 +42,9 @@
|
|||||||
</Table.Root>
|
</Table.Root>
|
||||||
|
|
||||||
<!--Floating action button for adding a new client-->
|
<!--Floating action button for adding a new client-->
|
||||||
<!--Not sure if this is the best place for the input field, will think about it later-->
|
<form class="self-end mt-auto pt-4" method="post" action="?/create">
|
||||||
<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 class="mr-2 h-4 w-4" />
|
<LucidePlus />
|
||||||
Add Client
|
Add Client
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
import type { ClientDetails } from '$lib/types/clients';
|
import type { Clients } from '../api/clients/+server';
|
||||||
|
|
||||||
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: ClientDetails[] };
|
const { clients } = await res.json() as { clients: Clients };
|
||||||
|
|
||||||
return { clients };
|
return { clients };
|
||||||
};
|
};
|
||||||
|
@ -1,24 +1,19 @@
|
|||||||
<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,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function copyToClipboard() {
|
function copyToClipboard() {
|
||||||
await navigator.clipboard.writeText(data.config);
|
navigator.clipboard.writeText(data.config).then(() => {
|
||||||
tooltipText = 'Copied!';
|
tooltipText = 'Copied';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
tooltipText = 'Copy to clipboard';
|
tooltipText = 'Copy to Clipboard';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -26,30 +21,21 @@
|
|||||||
<title></title>
|
<title></title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 class="bg-accent text-lg w-fit rounded-lg p-2 mb-4">{data.client.name}</h1>
|
<h1>Client: {data.client.name}</h1>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex relative bg-accent p-2 rounded-xl overflow-x-scroll">
|
||||||
<div class="relative bg-accent rounded-lg max-w-fit">
|
<pre><code>{data.config}</code></pre>
|
||||||
<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-->
|
||||||
<!--Flex reverse for peer hover to work properly-->
|
<div class="absolute flex right-2 items-center group">
|
||||||
<div class="absolute group flex flex-row-reverse items-center gap-1 right-2">
|
<span class="hidden group-hover:block bg-background text-xs rounded py-1 px-2">
|
||||||
<Button class="peer size-10 p-2"
|
{tooltipText}
|
||||||
onclick={copyToClipboard}
|
</span>
|
||||||
onmouseleave={onMouseLeave}
|
<button class="flex items-center justify-center w-10 h-10 bg-background rounded-xl ml-2"
|
||||||
>
|
onclick={copyToClipboard}
|
||||||
<LucideClipboardCopy />
|
onmouseleave="{onMouseLeave}"
|
||||||
</Button>
|
>
|
||||||
<span class="hidden peer-hover:block bg-background text-xs rounded-lg p-2">
|
<LucideClipboardCopy />
|
||||||
{tooltipText}
|
</button>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="rounded-lg overflow-hidden">
|
|
||||||
{@html qrCode.svg()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,15 +1,10 @@
|
|||||||
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 resJson = await res.json();
|
const client = (await res.json()) as ClientDetails;
|
||||||
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,24 +33,22 @@
|
|||||||
<title>Connections</title>
|
<title>Connections</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Table.Root class="bg-accent rounded-lg overflow-hidden divide-y-2 divide-background">
|
<Table.Root class="bg-accent rounded-xl">
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Head>Name</Table.Head>
|
||||||
<Table.Head scope="col">Name</Table.Head>
|
<Table.Head>Public Key</Table.Head>
|
||||||
<Table.Head scope="col">Public Key</Table.Head>
|
<Table.Head>Endpoint</Table.Head>
|
||||||
<Table.Head scope="col">Endpoint</Table.Head>
|
<Table.Head>Allowed IPs</Table.Head>
|
||||||
<Table.Head scope="col">Allowed IPs</Table.Head>
|
<Table.Head>Latest Handshake</Table.Head>
|
||||||
<Table.Head scope="col">Latest Handshake</Table.Head>
|
<Table.Head>RX</Table.Head>
|
||||||
<Table.Head scope="col">RX</Table.Head>
|
<Table.Head>TX</Table.Head>
|
||||||
<Table.Head scope="col">TX</Table.Head>
|
<Table.Head class="hidden">Persistent Keepalive</Table.Head>
|
||||||
<Table.Head scope="col" class="hidden">Persistent Keepalive</Table.Head>
|
<Table.Head class="hidden">Interface Name</Table.Head>
|
||||||
<Table.Head scope="col" class="hidden">Interface Name</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body class="divide-y-2 divide-background">
|
<Table.Body>
|
||||||
{#each data.peers.rows as peer}
|
{#each data.peers.rows as peer}
|
||||||
<Table.Row class="hover:bg-background hover:bg-opacity-40">
|
<Table.Row class="border-y-2 border-background">
|
||||||
<Table.Head scope="row">{peer.name}</Table.Head>
|
<Table.Cell>{peer.name}</Table.Cell>
|
||||||
<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