From 99f4016eb35f26a1d18bcefe53c1c872ef4e6d91 Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Thu, 9 Jan 2025 18:55:16 -0800 Subject: [PATCH] devices page: implement delete option --- bruno/opnsense-api/Delete Client.bru | 25 +++++++++ src/lib/server/devices/delete.ts | 73 +++++++++++++++++++++++++ src/routes/devices/+page.server.ts | 4 +- src/routes/devices/+page.svelte | 14 +++-- src/routes/devices/[id]/+page.server.ts | 23 ++++++++ src/routes/devices/[id]/+page.svelte | 6 +- src/routes/devices/delete-device.svelte | 37 +++++++++++++ 7 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 bruno/opnsense-api/Delete Client.bru create mode 100644 src/lib/server/devices/delete.ts create mode 100644 src/routes/devices/[id]/+page.server.ts create mode 100644 src/routes/devices/delete-device.svelte diff --git a/bruno/opnsense-api/Delete Client.bru b/bruno/opnsense-api/Delete Client.bru new file mode 100644 index 0000000..63605cf --- /dev/null +++ b/bruno/opnsense-api/Delete Client.bru @@ -0,0 +1,25 @@ +meta { + name: Delete Client + type: http + seq: 11 +} + +post { + url: {{base}}/api/wireguard/client/delClient/:clientUuid + body: none + auth: inherit +} + +params:path { + clientUuid: d484d381-4d6f-4444-8e9d-9cda7b5b2243 +} + +body:json { + { + "current": 1, + "rowCount": 7, + "sort": {}, + "servers": ["{{serverUuid}}"], + "searchPhrase": "{{searchPhrase}}" + } +} diff --git a/src/lib/server/devices/delete.ts b/src/lib/server/devices/delete.ts new file mode 100644 index 0000000..bebffc0 --- /dev/null +++ b/src/lib/server/devices/delete.ts @@ -0,0 +1,73 @@ +import { and, eq } from 'drizzle-orm'; +import { db } from '$lib/server/db'; +import { devices } from '$lib/server/db/schema'; +import { err, ok, type Result } from '$lib/types'; +import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense'; + +export async function deleteDevice( + userId: string, + deviceId: number, +): Promise> { + const device = await db.query.devices.findFirst({ + columns: { + publicKey: true, + }, + where: and(eq(devices.userId, userId), eq(devices.id, deviceId)), + }); + if (!device) return err([400, 'Device not found']); + const opnsenseClientUuid = (await opnsenseFindClient(device.publicKey))?.['uuid']; + if (typeof opnsenseClientUuid !== 'string') { + console.error( + 'failed to get OPNsense client for deletion for device', + deviceId, + device.publicKey, + ); + return err([500, 'Error getting client from OPNsense']); + } + + const opnsenseDeletionResult = await opnsenseDeleteClient(opnsenseClientUuid); + if (opnsenseDeletionResult?.['result'] !== 'deleted') { + console.error( + 'failed to delete OPNsense client for device', + deviceId, + device.publicKey, + '\n', + 'OPNsense client uuid:', + opnsenseClientUuid, + ); + return err([500, 'Error deleting client in OPNsense']); + } + + await db.delete(devices).where(eq(devices.id, deviceId)); + return ok(null); +} + +async function opnsenseFindClient(pubkey: string) { + const res = await fetch(`${opnsenseUrl}/api/wireguard/client/searchClient`, { + method: 'POST', + headers: { + Authorization: opnsenseAuth, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + current: 1, + // "rowCount": 7, + sort: {}, + searchPhrase: pubkey, + type: ['peer'], + }), + }); + return (await res.json())?.rows?.[0] ?? null; +} + +async function opnsenseDeleteClient(clientUuid: string) { + const res = await fetch(`${opnsenseUrl}/api/wireguard/client/delClient/${clientUuid}`, { + method: 'POST', + headers: { + Authorization: opnsenseAuth, + Accept: 'application/json', + }, + }); + return res.json(); +} diff --git a/src/routes/devices/+page.server.ts b/src/routes/devices/+page.server.ts index 44755de..4dcf0ce 100644 --- a/src/routes/devices/+page.server.ts +++ b/src/routes/devices/+page.server.ts @@ -1,13 +1,13 @@ import type { Actions } from './$types'; import { createDevice } from '$lib/server/devices'; -import { error, redirect } from '@sveltejs/kit'; +import { error, fail, redirect } from '@sveltejs/kit'; export const actions = { create: async (event) => { if (!event.locals.user) return error(401, 'Unauthorized'); const formData = await event.request.formData(); const name = formData.get('name'); - if (typeof name !== 'string' || name.trim() === '') return error(400, 'Invalid name'); + if (typeof name !== 'string' || name.trim() === '') return fail(400, { name, invalid: true }); const res = await createDevice({ name: name.trim(), user: event.locals.user, diff --git a/src/routes/devices/+page.svelte b/src/routes/devices/+page.svelte index 1c645c8..483043c 100644 --- a/src/routes/devices/+page.svelte +++ b/src/routes/devices/+page.svelte @@ -8,6 +8,7 @@ import type { PageData } from './$types'; import { Label } from '$lib/components/ui/label'; import { page } from '$app/state'; + import DeleteDevice from './delete-device.svelte'; const { data }: { data: PageData } = $props(); @@ -56,6 +57,9 @@ {ip} {/each} + + + {/each} @@ -86,11 +90,11 @@ class="max-w-[20ch]" /> - - diff --git a/src/routes/devices/[id]/+page.server.ts b/src/routes/devices/[id]/+page.server.ts new file mode 100644 index 0000000..a8df68d --- /dev/null +++ b/src/routes/devices/[id]/+page.server.ts @@ -0,0 +1,23 @@ +import { error, redirect } from '@sveltejs/kit'; +import { deleteDevice } from '$lib/server/devices/delete'; +import type { Actions } from './$types'; + +export const actions = { + delete: async (event) => { + if (!event.locals.user) return error(401, 'Unauthorized'); + + const deviceId = Number.parseInt(event.params.id); + if (Number.isNaN(deviceId)) return error(400, 'Invalid device id'); + + const res = await deleteDevice(event.locals.user.id, deviceId); + switch (res._tag) { + case 'ok': { + return redirect(303, '/devices'); + } + case 'err': { + const [status, message] = res.error; + return error(status, message); + } + } + }, +} satisfies Actions; diff --git a/src/routes/devices/[id]/+page.svelte b/src/routes/devices/[id]/+page.svelte index 7705510..9b002e7 100644 --- a/src/routes/devices/[id]/+page.svelte +++ b/src/routes/devices/[id]/+page.svelte @@ -3,6 +3,7 @@ import QRCode from 'qrcode-svg'; import { CodeSnippet } from '$lib/components/app/code-snippet'; import { WireguardGuide } from '$lib/components/app/wireguard-guide'; + import DeleteDevice from '../delete-device.svelte'; const { data }: { data: PageData } = $props(); @@ -25,7 +26,10 @@ {data.device.name} -

{data.device.name}

+
+

{data.device.name}

+ +
diff --git a/src/routes/devices/delete-device.svelte b/src/routes/devices/delete-device.svelte new file mode 100644 index 0000000..57012ad --- /dev/null +++ b/src/routes/devices/delete-device.svelte @@ -0,0 +1,37 @@ + + + + + + + +
submitted = true} + > + + Are you sure you want to permanently delete "{device.name}"? + + You will no longer be able to connect from this device. + + {#if submitted} + + {/if} + + + + + +
+
+