rename clients to devices
This commit is contained in:
parent
d99ee9ef1e
commit
cc7c94417d
@ -1,10 +1,22 @@
|
||||
CREATE TABLE `ip_allocations` (
|
||||
CREATE TABLE `devices` (
|
||||
`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
|
||||
`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 `ip_allocations_client_id_unique` ON `ip_allocations` (`client_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `devices_public_key_unique` ON `devices` (`public_key`);--> statement-breakpoint
|
||||
CREATE TABLE `ip_allocations` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`device_id` integer,
|
||||
FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `ip_allocations_device_id_unique` ON `ip_allocations` (`device_id`);--> statement-breakpoint
|
||||
CREATE TABLE `sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
@ -17,16 +29,3 @@ CREATE TABLE `users` (
|
||||
`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,9 +1,90 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "29e6fd88-fa47-4f79-ad83-c52538bc36a6",
|
||||
"id": "48b7ce55-58f1-4b97-a144-ca733576dba6",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"devices": {
|
||||
"name": "devices",
|
||||
"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": {
|
||||
"devices_public_key_unique": {
|
||||
"name": "devices_public_key_unique",
|
||||
"columns": [
|
||||
"public_key"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"devices_user_id_users_id_fk": {
|
||||
"name": "devices_user_id_users_id_fk",
|
||||
"tableFrom": "devices",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"ip_allocations": {
|
||||
"name": "ip_allocations",
|
||||
"columns": {
|
||||
@ -14,8 +95,8 @@
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"client_id": {
|
||||
"name": "client_id",
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
@ -23,21 +104,21 @@
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"ip_allocations_client_id_unique": {
|
||||
"name": "ip_allocations_client_id_unique",
|
||||
"ip_allocations_device_id_unique": {
|
||||
"name": "ip_allocations_device_id_unique",
|
||||
"columns": [
|
||||
"client_id"
|
||||
"device_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"ip_allocations_client_id_wg_clients_id_fk": {
|
||||
"name": "ip_allocations_client_id_wg_clients_id_fk",
|
||||
"ip_allocations_device_id_devices_id_fk": {
|
||||
"name": "ip_allocations_device_id_devices_id_fk",
|
||||
"tableFrom": "ip_allocations",
|
||||
"tableTo": "wg_clients",
|
||||
"tableTo": "devices",
|
||||
"columnsFrom": [
|
||||
"client_id"
|
||||
"device_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
@ -125,87 +206,6 @@
|
||||
"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": {},
|
||||
|
@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1735028333867,
|
||||
"tag": "0000_young_wong",
|
||||
"when": 1736295566569,
|
||||
"tag": "0000_fair_tarantula",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
@ -39,7 +39,7 @@ const authRequired = [
|
||||
/^\/api/,
|
||||
/^\/user/,
|
||||
/^\/connections/,
|
||||
/^\/clients/,
|
||||
/^\/devices/,
|
||||
];
|
||||
const handleProtectedPaths: Handle = ({ event, resolve }) => {
|
||||
const isProtected = authRequired.some((re) => re.test(event.url.pathname));
|
||||
|
@ -1,33 +0,0 @@
|
||||
import type { ClientDetails } from '$lib/types/clients';
|
||||
|
||||
/**
|
||||
* Convert client details to WireGuard configuration.
|
||||
*
|
||||
* ```conf
|
||||
* [Interface]
|
||||
* PrivateKey = wPa07zR0H4wYoc1ljfeiqlSbR8Z28pPc6jplwE7zPms=
|
||||
* Address = 10.18.11.100/32,fd00::1/128
|
||||
* DNS = 10.18.11.1,fd00::0
|
||||
*
|
||||
* [Peer]
|
||||
* PublicKey = BJ5faPVJsDP4CCxNYilmKnwlQXOtXEOJjqIwb4U/CgM=
|
||||
* PresharedKey = uhZUVqXKF0oayP0BS6yPu6Gepgh68Nz9prtbE5Cuok0=
|
||||
* Endpoint = vpn.lab.cazzzer.com:51820
|
||||
* AllowedIPs = 0.0.0.0/0,::/0
|
||||
* ```
|
||||
* @param client
|
||||
*/
|
||||
export function clientDetailsToConfig(client: ClientDetails): string {
|
||||
return `\
|
||||
[Interface]
|
||||
PrivateKey = ${client.privateKey}
|
||||
Address = ${client.ips.join(', ')}
|
||||
DNS = ${client.vpnDns}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${client.vpnPublicKey}
|
||||
PresharedKey = ${client.preSharedKey}
|
||||
Endpoint = ${client.vpnEndpoint}
|
||||
AllowedIPs = 0.0.0.0/0,::/0
|
||||
`;
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import getItOnGooglePlay from '$lib/assets/GetItOnGooglePlay_Badge_Web_color_English.png';
|
||||
import guideVideo from '$lib/assets/guide-client.mp4';
|
||||
import guideVideoAndroid from '$lib/assets/guide-android.mp4';
|
||||
</script>
|
||||
|
||||
<Tabs.Root value="android">
|
||||
@ -36,7 +36,7 @@
|
||||
<p>Download the configuration file and import it</p>
|
||||
<aside>Alternatively, you can scan the QR code with the WireGuard app</aside>
|
||||
<video autoplay loop controls muted preload="metadata" class="max-h-screen">
|
||||
<source src={guideVideo} type="video/mp4" />
|
||||
<source src={guideVideoAndroid} type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</li>
|
||||
|
43
src/lib/devices.ts
Normal file
43
src/lib/devices.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Convert device details to WireGuard configuration.
|
||||
*
|
||||
* ```conf
|
||||
* [Interface]
|
||||
* PrivateKey = wPa07zR0H4wYoc1ljfeiqlSbR8Z28pPc6jplwE7zPms=
|
||||
* Address = 10.18.11.100/32,fd00::1/128
|
||||
* DNS = 10.18.11.1,fd00::0
|
||||
*
|
||||
* [Peer]
|
||||
* PublicKey = BJ5faPVJsDP4CCxNYilmKnwlQXOtXEOJjqIwb4U/CgM=
|
||||
* PresharedKey = uhZUVqXKF0oayP0BS6yPu6Gepgh68Nz9prtbE5Cuok0=
|
||||
* Endpoint = vpn.lab.cazzzer.com:51820
|
||||
* AllowedIPs = 0.0.0.0/0,::/0
|
||||
* ```
|
||||
* @param device
|
||||
*/
|
||||
export function deviceDetailsToConfig(device: DeviceDetails): string {
|
||||
return `\
|
||||
[Interface]
|
||||
PrivateKey = ${device.privateKey}
|
||||
Address = ${device.ips.join(', ')}
|
||||
DNS = ${device.vpnDns}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${device.vpnPublicKey}
|
||||
PresharedKey = ${device.preSharedKey}
|
||||
Endpoint = ${device.vpnEndpoint}
|
||||
AllowedIPs = 0.0.0.0/0,::/0
|
||||
`;
|
||||
}
|
||||
|
||||
export type DeviceDetails = {
|
||||
id: number;
|
||||
name: string;
|
||||
publicKey: string;
|
||||
privateKey: string | null;
|
||||
preSharedKey: string | null;
|
||||
ips: string[];
|
||||
vpnPublicKey: string;
|
||||
vpnEndpoint: string;
|
||||
vpnDns: string;
|
||||
};
|
@ -8,7 +8,7 @@ export const users = sqliteTable('users', {
|
||||
});
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
wgClients: many(wgClients),
|
||||
devices: many(devices),
|
||||
}));
|
||||
|
||||
export const sessions = sqliteTable('sessions', {
|
||||
@ -22,14 +22,14 @@ export const sessions = sqliteTable('sessions', {
|
||||
export const ipAllocations = sqliteTable('ip_allocations', {
|
||||
// for now, id will be the same as the ipIndex
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
// clientId is nullable because allocations can remain after the client is deleted
|
||||
// unique for now, only allowing one allocation per client
|
||||
clientId: integer('client_id')
|
||||
// deviceId is nullable because allocations can remain after the device is deleted
|
||||
// unique for now, only allowing one allocation per device
|
||||
deviceId: integer('device_id')
|
||||
.unique()
|
||||
.references(() => wgClients.id, { onDelete: 'set null' }),
|
||||
.references(() => devices.id, { onDelete: 'set null' }),
|
||||
});
|
||||
|
||||
export const wgClients = sqliteTable('wg_clients', {
|
||||
export const devices = sqliteTable('devices', {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
@ -38,7 +38,7 @@ export const wgClients = sqliteTable('wg_clients', {
|
||||
// questioning whether this should be nullable
|
||||
opnsenseId: text('opnsense_id'),
|
||||
publicKey: text('public_key').notNull().unique(),
|
||||
// nullable for the possibility of a client supplying their own private key
|
||||
// nullable for the possibility of a user supplying their own private key
|
||||
privateKey: text('private_key'),
|
||||
// nullable for the possibility of no psk
|
||||
preSharedKey: text('pre_shared_key'),
|
||||
@ -48,18 +48,18 @@ export const wgClients = sqliteTable('wg_clients', {
|
||||
// allowedIps: text('allowed_ips').notNull(),
|
||||
});
|
||||
|
||||
export const wgClientsRelations = relations(wgClients, ({ one }) => ({
|
||||
export const devicesRelations = relations(devices, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [wgClients.userId],
|
||||
fields: [devices.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
ipAllocation: one(ipAllocations, {
|
||||
fields: [wgClients.id],
|
||||
references: [ipAllocations.clientId],
|
||||
fields: [devices.id],
|
||||
references: [ipAllocations.deviceId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type WgClient = typeof wgClients.$inferSelect;
|
||||
export type Device = typeof devices.$inferSelect;
|
||||
|
||||
export type Session = typeof sessions.$inferSelect;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ipAllocations, users, wgClients } from './schema';
|
||||
import { ipAllocations, users, devices } from './schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import assert from 'node:assert';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
@ -11,10 +11,10 @@ async function seed() {
|
||||
const user = await db.query.users.findFirst({ where: eq(users.username, 'CaZzzer') });
|
||||
assert(user, 'User not found');
|
||||
|
||||
const clients: typeof wgClients.$inferInsert[] = [
|
||||
const newDevices: typeof devices.$inferInsert[] = [
|
||||
{
|
||||
userId: user.id,
|
||||
name: 'Client1',
|
||||
name: 'Device1',
|
||||
publicKey: 'BJ5faPVJsDP4CCxNYilmKnwlQXOtXEOJjqIwb4U/CgM=',
|
||||
privateKey: 'KKqsHDu30WCSrVsyzMkOKbE3saQ+wlx0sBwGs61UGXk=',
|
||||
preSharedKey: '0LWopbrISXBNHUxr+WOhCSAg+0hD8j3TLmpyzHkBHCQ=',
|
||||
@ -22,10 +22,10 @@ async function seed() {
|
||||
// allowedIps: '10.18.11.101/32,fd00::1/112',
|
||||
},
|
||||
];
|
||||
const returned = await db.insert(wgClients).values(clients).returning({ insertedId: wgClients.id });
|
||||
const returned = await db.insert(devices).values(newDevices).returning({ insertedId: devices.id });
|
||||
|
||||
const ipAllocation: typeof ipAllocations.$inferInsert = {
|
||||
clientId: returned[0].insertedId,
|
||||
deviceId: returned[0].insertedId,
|
||||
};
|
||||
await db.insert(ipAllocations).values(ipAllocation);
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import type { User } from '$lib/server/db/schema';
|
||||
import { ipAllocations, wgClients } from '$lib/server/db/schema';
|
||||
import { ipAllocations, devices } from '$lib/server/db/schema';
|
||||
import { db } from '$lib/server/db';
|
||||
import { opnsenseAuth, opnsenseUrl, serverPublicKey, serverUuid } from '$lib/server/opnsense';
|
||||
import { Address4, Address6 } from 'ip-address';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { and, count, eq, isNull } from 'drizzle-orm';
|
||||
import { err, ok, type Result } from '$lib/types';
|
||||
import type { ClientDetails } from '$lib/types/clients';
|
||||
import type { DeviceDetails } from '$lib/devices';
|
||||
|
||||
export async function findClients(userId: string) {
|
||||
return db.query.wgClients.findMany({
|
||||
export async function findDevices(userId: string) {
|
||||
return db.query.devices.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
@ -20,12 +20,12 @@ export async function findClients(userId: string) {
|
||||
with: {
|
||||
ipAllocation: true,
|
||||
},
|
||||
where: eq(wgClients.userId, userId),
|
||||
where: eq(devices.userId, userId),
|
||||
});
|
||||
}
|
||||
|
||||
export async function findClient(userId: string, clientId: number) {
|
||||
return db.query.wgClients.findFirst({
|
||||
export async function findDevice(userId: string, deviceId: number) {
|
||||
return db.query.devices.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
@ -36,20 +36,20 @@ export async function findClient(userId: string, clientId: number) {
|
||||
with: {
|
||||
ipAllocation: true,
|
||||
},
|
||||
where: and(eq(wgClients.userId, userId), eq(wgClients.id, clientId)),
|
||||
where: and(eq(devices.userId, userId), eq(devices.id, deviceId)),
|
||||
});
|
||||
}
|
||||
|
||||
export function mapClientToDetails(
|
||||
client: Awaited<ReturnType<typeof findClients>>[0],
|
||||
): ClientDetails {
|
||||
const ips = getIpsFromIndex(client.ipAllocation.id);
|
||||
export function mapDeviceToDetails(
|
||||
device: Awaited<ReturnType<typeof findDevices>>[0],
|
||||
): DeviceDetails {
|
||||
const ips = getIpsFromIndex(device.ipAllocation.id);
|
||||
return {
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
publicKey: client.publicKey,
|
||||
privateKey: client.privateKey,
|
||||
preSharedKey: client.preSharedKey,
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
publicKey: device.publicKey,
|
||||
privateKey: device.privateKey,
|
||||
preSharedKey: device.preSharedKey,
|
||||
ips,
|
||||
vpnPublicKey: serverPublicKey,
|
||||
vpnEndpoint: env.VPN_ENDPOINT,
|
||||
@ -57,35 +57,35 @@ export function mapClientToDetails(
|
||||
};
|
||||
}
|
||||
|
||||
export async function createClient(params: {
|
||||
export async function createDevice(params: {
|
||||
name: string;
|
||||
user: User;
|
||||
}): Promise<Result<number, [400 | 500, string]>> {
|
||||
// check if user exceeds the limit of clients
|
||||
const [{ clientCount }] = await db
|
||||
.select({ clientCount: count() })
|
||||
.from(wgClients)
|
||||
.where(eq(wgClients.userId, params.user.id));
|
||||
if (clientCount >= parseInt(env.MAX_CLIENTS_PER_USER))
|
||||
return err([400, 'Maximum number of clients reached'] as [400, string]);
|
||||
// check if user exceeds the limit of devices
|
||||
const [{ deviceCount }] = await db
|
||||
.select({ deviceCount: count() })
|
||||
.from(devices)
|
||||
.where(eq(devices.userId, params.user.id));
|
||||
if (deviceCount >= parseInt(env.MAX_CLIENTS_PER_USER))
|
||||
return err([400, 'Maximum number of devices reached'] as [400, string]);
|
||||
|
||||
// this is going to be quite long
|
||||
// 1. fetch params for new client from opnsense api
|
||||
// 2.1 get an allocation for the client
|
||||
// 2.2. insert new client into db
|
||||
// 2.3. update the allocation with the client id
|
||||
// 1. fetch params for new device from opnsense api
|
||||
// 2.1 get an allocation for the device
|
||||
// 2.2. insert new device into db
|
||||
// 2.3. update the allocation with the device id
|
||||
// 3. create the client in opnsense
|
||||
// 4. reconfigure opnsense to enable the new client
|
||||
return await db.transaction(async (tx) => {
|
||||
const [keys, availableAllocation, lastAllocation] = await Promise.all([
|
||||
// fetch params for new client from opnsense api
|
||||
// fetch params for new device from opnsense api
|
||||
getKeys(),
|
||||
// find first unallocated IP
|
||||
await tx.query.ipAllocations.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
where: isNull(ipAllocations.clientId),
|
||||
where: isNull(ipAllocations.deviceId),
|
||||
}),
|
||||
// find last allocation to check if we have any IPs left
|
||||
await tx.query.ipAllocations.findFirst({
|
||||
@ -109,9 +109,9 @@ export async function createClient(params: {
|
||||
// transaction savepoint after creating a new IP allocation
|
||||
// TODO: not sure if this is needed
|
||||
return await tx.transaction(async (tx2) => {
|
||||
// create new client in db
|
||||
const [newClient] = await tx2
|
||||
.insert(wgClients)
|
||||
// create new device in db
|
||||
const [newDevice] = await tx2
|
||||
.insert(devices)
|
||||
.values({
|
||||
userId: params.user.id,
|
||||
name: params.name,
|
||||
@ -119,12 +119,12 @@ export async function createClient(params: {
|
||||
privateKey: keys.privkey,
|
||||
preSharedKey: keys.psk,
|
||||
})
|
||||
.returning({ id: wgClients.id });
|
||||
.returning({ id: devices.id });
|
||||
|
||||
// update IP allocation with client ID
|
||||
// update IP allocation with device ID
|
||||
await tx2
|
||||
.update(ipAllocations)
|
||||
.set({ clientId: newClient.id })
|
||||
.set({ deviceId: newDevice.id })
|
||||
.where(eq(ipAllocations.id, ipAllocationId));
|
||||
|
||||
// create client in opnsense
|
||||
@ -143,7 +143,7 @@ export async function createClient(params: {
|
||||
|
||||
// reconfigure opnsense
|
||||
await opnsenseReconfigure();
|
||||
return ok(newClient.id);
|
||||
return ok(newDevice.id);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export type ClientDetails = {
|
||||
id: number;
|
||||
name: string;
|
||||
publicKey: string;
|
||||
privateKey: string | null;
|
||||
preSharedKey: string | null;
|
||||
ips: string[];
|
||||
vpnPublicKey: string;
|
||||
vpnEndpoint: string;
|
||||
vpnDns: string;
|
||||
};
|
0
src/lib/types/device.ts
Normal file
0
src/lib/types/device.ts
Normal file
@ -24,7 +24,7 @@
|
||||
{#if user}
|
||||
<li><a href="/user" class={getNavClass(/^\/user$/)}>Profile</a></li>
|
||||
<li><a href="/connections" class={getNavClass(/^\/connections$/)}>Connections</a></li>
|
||||
<li><a href="/clients" class={getNavClass(/^\/clients(\/\d+)?$/)}>Clients</a></li>
|
||||
<li><a href="/devices" class={getNavClass(/^\/devices(\/\d+)?$/)}>Devices</a></li>
|
||||
{/if}
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -21,7 +21,7 @@
|
||||
<section id="get-started" class="border-l-2 pl-6">
|
||||
<p>
|
||||
To get started,
|
||||
<Button class="ml-2" href="/clients?add=New+Client">Create a New Client</Button>
|
||||
<Button class="ml-2" href="/devices?add=New+Device">Add a New Device</Button>
|
||||
</p>
|
||||
</section>
|
||||
<!-- <section id="using-wireguard">-->
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { findClient, mapClientToDetails } from '$lib/server/clients';
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const { id } = event.params;
|
||||
const clientId = parseInt(id);
|
||||
if (isNaN(clientId)) {
|
||||
return error(400, 'Invalid client ID');
|
||||
}
|
||||
const client = await findClient(event.locals.user.id, clientId);
|
||||
if (!client) {
|
||||
return error(404, 'Client not found');
|
||||
}
|
||||
return new Response(JSON.stringify(mapClientToDetails(client)));
|
||||
};
|
@ -1,29 +1,29 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { createClient, findClients, mapClientToDetails } from '$lib/server/clients';
|
||||
import { createDevice, findDevices, mapDeviceToDetails } from '$lib/server/devices';
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const clients = await findClients(event.locals.user.id);
|
||||
const devices = await findDevices(event.locals.user.id);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
clients: clients.map(mapClientToDetails),
|
||||
devices: devices.map(mapDeviceToDetails),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export type Clients = Awaited<ReturnType<typeof findClients>>;
|
||||
export type Devices = Awaited<ReturnType<typeof findDevices>>;
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return error(401, 'Unauthorized');
|
||||
}
|
||||
const { name } = await event.request.json();
|
||||
const res = await createClient({
|
||||
const res = await createDevice({
|
||||
name,
|
||||
user: event.locals.user,
|
||||
});
|
20
src/routes/api/devices/[id]/+server.ts
Normal file
20
src/routes/api/devices/[id]/+server.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { findDevice, mapDeviceToDetails } from '$lib/server/devices';
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const { id } = event.params;
|
||||
const deviceId = parseInt(id);
|
||||
if (isNaN(deviceId)) {
|
||||
return error(400, 'Invalid device ID');
|
||||
}
|
||||
const device = await findDevice(event.locals.user.id, deviceId);
|
||||
if (!device) {
|
||||
return error(404, 'Device not found');
|
||||
}
|
||||
return new Response(JSON.stringify(mapDeviceToDetails(device)));
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
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: ClientDetails[] };
|
||||
|
||||
return { clients };
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
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 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 };
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import type { Actions } from './$types';
|
||||
import { createClient } from '$lib/server/clients';
|
||||
import { createDevice } from '$lib/server/devices';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const actions = {
|
||||
@ -8,14 +8,14 @@ export const actions = {
|
||||
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 createDevice({
|
||||
name: name.trim(),
|
||||
user: event.locals.user,
|
||||
});
|
||||
|
||||
switch (res._tag) {
|
||||
case 'ok': {
|
||||
return redirect(303, `/clients/${res.value}`);
|
||||
return redirect(303, `/devices/${res.value}`);
|
||||
}
|
||||
case 'err': {
|
||||
const [status, message] = res.error;
|
@ -23,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Clients</title>
|
||||
<title>Devices</title>
|
||||
</svelte:head>
|
||||
|
||||
<Table.Root class="divide-y-2 divide-background overflow-hidden rounded-lg bg-accent">
|
||||
@ -37,21 +37,21 @@
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body class="divide-y-2 divide-background">
|
||||
{#each data.clients as client}
|
||||
{#each data.devices as device}
|
||||
<Table.Row class="hover:bg-surface group">
|
||||
<Table.Head scope="row">
|
||||
<a
|
||||
href={`/clients/${client.id}`}
|
||||
href={`/devices/${device.id}`}
|
||||
class="flex size-full items-center group-hover:underline"
|
||||
>
|
||||
{client.name}
|
||||
{device.name}
|
||||
</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="truncate">{device.publicKey}</Table.Cell>
|
||||
<!-- <Table.Cell class="truncate max-w-[10ch]">{device.privateKey}</Table.Cell>-->
|
||||
<!-- <Table.Cell class="truncate max-w-[10ch]">{device.preSharedKey}</Table.Cell>-->
|
||||
<Table.Cell class="flex flex-wrap gap-1">
|
||||
{#each client.ips as ip}
|
||||
{#each device.ips as ip}
|
||||
<Badge class="select-auto bg-background" variant="secondary">{ip}</Badge>
|
||||
{/each}
|
||||
</Table.Cell>
|
||||
@ -60,18 +60,18 @@
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
|
||||
<!--Floating action button for adding a new client-->
|
||||
<!--Floating action button for adding a new device-->
|
||||
<Dialog.Root bind:open={dialogOpen}>
|
||||
<div class="mt-auto flex self-end pt-4">
|
||||
<Dialog.Trigger class={buttonVariants({ variant: 'default' }) + ' flex gap-4'}>
|
||||
<LucidePlus />
|
||||
New Client
|
||||
New Device
|
||||
</Dialog.Trigger>
|
||||
</div>
|
||||
<Dialog.Content class="max-w-xs">
|
||||
<form class="contents" method="post" action="?/create">
|
||||
<Dialog.Header class="">
|
||||
<Dialog.Title>Create a new client</Dialog.Title>
|
||||
<Dialog.Title>Add a new device</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<Label for="name">Name</Label>
|
||||
@ -81,12 +81,12 @@
|
||||
pattern=".*[^\s]+.*"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="New Client"
|
||||
placeholder="New Device"
|
||||
class="max-w-[20ch]"
|
||||
/>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button type="submit">Create</Button>
|
||||
<Button type="submit">Add</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
9
src/routes/devices/+page.ts
Normal file
9
src/routes/devices/+page.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import type { DeviceDetails } from '$lib/devices';
|
||||
|
||||
export const load: PageLoad = async ({ fetch }) => {
|
||||
const res = await fetch('/api/devices');
|
||||
const { devices } = await res.json() as { devices: DeviceDetails[] };
|
||||
|
||||
return { devices };
|
||||
};
|
@ -6,11 +6,11 @@
|
||||
|
||||
const { data }: { data: PageData } = $props();
|
||||
|
||||
// Clean the client name for the file name,
|
||||
// Clean the device name for the wg config filename,
|
||||
// things can break otherwise (too long or invalid characters)
|
||||
// https://github.com/pirate/wireguard-docs
|
||||
const clientWgCleanedName =
|
||||
data.client.name.slice(0, 15).replace(/[^a-zA-Z0-9_=+.-]/g, '_') + '.conf';
|
||||
const deviceWgCleanedName =
|
||||
data.device.name.slice(0, 15).replace(/[^a-zA-Z0-9_=+.-]/g, '_') + '.conf';
|
||||
|
||||
let qrCode = new QRCode({
|
||||
content: data.config,
|
||||
@ -22,13 +22,13 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.client.name}</title>
|
||||
<title>{data.device.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="w-fit rounded-lg bg-accent p-2 text-lg">{data.client.name}</h1>
|
||||
<h1 class="w-fit rounded-lg bg-accent p-2 text-lg">{data.device.name}</h1>
|
||||
|
||||
<section id="client-configuration" class="flex flex-wrap items-center justify-center gap-4">
|
||||
<CodeSnippet data={data.config} filename={clientWgCleanedName} copy download />
|
||||
<section id="device-configuration" class="flex flex-wrap items-center justify-center gap-4">
|
||||
<CodeSnippet data={data.config} filename={deviceWgCleanedName} copy download />
|
||||
|
||||
<div class="size-fit overflow-auto rounded-lg">
|
||||
{@html qrCode.svg()}
|
15
src/routes/devices/[id]/+page.ts
Normal file
15
src/routes/devices/[id]/+page.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { type DeviceDetails, deviceDetailsToConfig } from '$lib/devices';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const res = await fetch(`/api/devices/${params.id}`);
|
||||
const resJson = await res.json();
|
||||
if (!res.ok) {
|
||||
return error(res.status, resJson['message']);
|
||||
}
|
||||
const device = resJson as DeviceDetails;
|
||||
const config = deviceDetailsToConfig(device);
|
||||
|
||||
return { device, config };
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user