rename clients to devices

This commit is contained in:
2025-01-07 16:20:40 -08:00
parent d99ee9ef1e
commit cc7c94417d
25 changed files with 284 additions and 287 deletions

View File

@@ -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
`;
}

View File

@@ -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
View 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;
};

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
});
});
}

View File

@@ -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
View File