diff --git a/web/src/lib/typings.ts b/web/src/lib/typings.ts index 89235cc..f9fa7a9 100644 --- a/web/src/lib/typings.ts +++ b/web/src/lib/typings.ts @@ -41,6 +41,8 @@ export const PeerSchema = z export type Peer = z.infer; +export const WgServerStatusSchema = z.enum(['up', 'down']); + export const WgServerSchema = z .object({ id: z.string().uuid(), @@ -58,7 +60,7 @@ export const WgServerSchema = z peers: z.array(PeerSchema), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), - status: z.enum(['up', 'down']), + status: WgServerStatusSchema, }) .merge(WgKeySchema.omit({ preSharedKey: true })); diff --git a/web/src/lib/wireguard/index.ts b/web/src/lib/wireguard/index.ts index a73663f..425d9ad 100644 --- a/web/src/lib/wireguard/index.ts +++ b/web/src/lib/wireguard/index.ts @@ -1,7 +1,8 @@ import fs from 'fs'; import path from 'path'; import deepmerge from 'deepmerge'; -import { enc, SHA256 } from 'crypto-js'; +import SHA256 from 'crypto-js/sha256'; +import Hex from 'crypto-js/enc-hex'; import type { Peer, WgKey, WgServer } from '$lib/typings'; import Network from '$lib/network'; import Shell from '$lib/shell'; @@ -57,7 +58,9 @@ export class WGServer { } await this.stop(id); - fs.unlinkSync(path.join(WG_PATH, `wg${server.confId}.conf`)); + if (wgConfExists(server.confId)) { + fs.unlinkSync(path.join(WG_PATH, `wg${server.confId}.conf`)); + } const index = await findServerIndex(id); if (typeof index !== 'number') { @@ -538,7 +541,7 @@ export function getConfigHash(confId: number): string | undefined { const confPath = path.join(WG_PATH, `wg${confId}.conf`); const conf = fs.readFileSync(confPath, 'utf-8'); - return enc.Hex.stringify(SHA256(conf)); + return Hex.stringify(SHA256(conf)); } export async function writeConfigFile(wg: WgServer): Promise { diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.server.ts index 1cb8d3d..3c67465 100644 --- a/web/src/routes/+page.server.ts +++ b/web/src/routes/+page.server.ts @@ -71,7 +71,8 @@ export const actions: Actions = { serverId, }; } catch (e: any) { - return setError(form, e.message); + console.error('Exception:', e); + return setError(form, 'Unhandled Exception'); } }, }; diff --git a/web/src/routes/CreateServerDialog.svelte b/web/src/routes/CreateServerDialog.svelte index effe495..9a88ad6 100644 --- a/web/src/routes/CreateServerDialog.svelte +++ b/web/src/routes/CreateServerDialog.svelte @@ -14,7 +14,6 @@ } from '$lib/components/ui/form'; import { goto } from '$app/navigation'; import { FormItem } from '$lib/components/ui/form/index.js'; - import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte'; import { cn } from '$lib/utils'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible'; import { Button } from '$lib/components/ui/button'; @@ -30,7 +29,6 @@ Create Server -
{ + loading = false; console.error('Client-side: FormError:', e); }, onResult: ({ result }) => { diff --git a/web/src/routes/Server.svelte b/web/src/routes/Server.svelte index 82368bd..10f32cc 100644 --- a/web/src/routes/Server.svelte +++ b/web/src/routes/Server.svelte @@ -43,15 +43,11 @@
- - {#if server.status === 'up'} - - Up - {:else} - - Down - {/if} - + {#if server.status === 'up'} + Online + {:else} + Offline + {/if}
diff --git a/web/src/routes/[serverId]/+page.server.ts b/web/src/routes/[serverId]/+page.server.ts index a415d3c..1f746ea 100644 --- a/web/src/routes/[serverId]/+page.server.ts +++ b/web/src/routes/[serverId]/+page.server.ts @@ -1,7 +1,10 @@ import { type Actions, error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { findServer, WGServer } from '$lib/wireguard'; +import { findServer, generateWgKey, WGServer } from '$lib/wireguard'; import { NameSchema } from '$lib/wireguard/schema'; +import { setError, superValidate } from 'sveltekit-superforms/server'; +import { CreatePeerSchema } from './schema'; +import { WgServerStatusSchema } from '$lib/typings'; export const load: PageServerLoad = async ({ params }) => { const { serverId } = params; @@ -17,30 +20,156 @@ export const load: PageServerLoad = async ({ params }) => { export const actions: Actions = { rename: async ({ request, params }) => { const { serverId } = params; - const server = await findServer(serverId ?? ''); if (!server) { console.error('Server not found'); - return error(404, 'Not found'); + throw error(404, 'Not found'); } - const form = await request.formData(); const peerId = (form.get('id') ?? '').toString(); const peer = server.peers.find((p) => p.id === peerId); if (!peer) { console.error('Peer not found'); - return error(404, 'Not found'); + throw error(404, 'Not found'); } const name = (form.get('name') ?? '').toString(); if (!NameSchema.safeParse(name).success) { console.error('Peer name is invalid'); - return error(400, 'Bad Request'); + throw error(400, 'Bad Request'); } - await WGServer.updatePeer(server.id, peer.publicKey, { name }); + try { + await WGServer.updatePeer(server.id, peer.publicKey, { name }); - return { ok: true }; + return { ok: true }; + } catch (e) { + console.error('Exception:', e); + throw error(500, 'Unhandled Exception'); + } + }, + remove: async ({ request, params }) => { + const { serverId } = params; + + const server = await findServer(serverId ?? ''); + if (!server) { + console.error('Server not found'); + throw error(404, 'Not found'); + } + + try { + const form = await request.formData(); + const peerId = (form.get('id') ?? '').toString(); + const peer = server.peers.find((p) => p.id === peerId); + if (peer) { + await WGServer.removePeer(server.id, peer.publicKey); + } + + return { ok: true }; + } catch (e) { + console.error('Exception:', e); + throw error(500, 'Unhandled Exception'); + } + }, + 'remove-server': async ({ params }) => { + const { serverId } = params; + + try { + const server = await findServer(serverId ?? ''); + if (server) { + await WGServer.remove(server.id); + } + + return { ok: true }; + } catch (e) { + console.error('Exception:', e); + throw error(500, 'Unhandled Exception'); + } + }, + 'change-server-state': async ({ request, params }) => { + const { serverId } = params; + + const server = await findServer(serverId ?? ''); + if (!server) { + console.error('Server not found'); + throw error(404, 'Not found'); + } + + const form = await request.formData(); + const status = (form.get('state') ?? '').toString(); + + try { + if (server.status !== status) { + switch (status) { + case 'start': + await WGServer.start(server.id); + break; + + case 'stop': + await WGServer.stop(server.id); + break; + + case 'remove': + await WGServer.remove(server.id); + break; + + case 'restart': + await WGServer.stop(server.id); + await WGServer.start(server.id); + break; + } + } + + return { ok: true }; + } catch (e) { + console.error('Exception:', e); + throw error(500, 'Unhandled Exception'); + } + }, + create: async (event) => { + const form = await superValidate(event, CreatePeerSchema); + if (!form.valid) { + return setError(form, 'Bad Request'); + } + + const { serverId } = event.params; + const { name } = form.data; + + try { + const server = await findServer(serverId ?? ''); + if (!server) { + console.error('Server not found'); + return setError(form, 'Server not found'); + } + + const freeAddress = await WGServer.getFreePeerIp(server.id); + if (!freeAddress) { + console.error(`ERR: ServerId: ${serverId};`, 'No free addresses;'); + return setError(form, 'No free addresses'); + } + + const peerKeys = await generateWgKey(); + + const addedPeer = await WGServer.addPeer(server.id, { + id: crypto.randomUUID(), + name, + allowedIps: freeAddress, + publicKey: peerKeys.publicKey, + privateKey: peerKeys.privateKey, + preSharedKey: peerKeys.preSharedKey, + persistentKeepalive: 0, + }); + + if (!addedPeer) { + console.error(`ERR: ServerId: ${serverId};`, 'Failed to add peer;'); + return setError(form, 'Failed to add peer'); + } + + return { ok: true }; + } catch (e) { + console.error('Exception:', e); + return setError(form, 'Unhandled Exception'); + } }, }; diff --git a/web/src/routes/[serverId]/+page.svelte b/web/src/routes/[serverId]/+page.svelte index 576d84a..2106a5c 100644 --- a/web/src/routes/[serverId]/+page.svelte +++ b/web/src/routes/[serverId]/+page.svelte @@ -6,6 +6,11 @@ import { Button } from '$lib/components/ui/button'; import Peer from './Peer.svelte'; import fetchAction from '$lib/fetch-action'; + import DetailRow from './DetailRow.svelte'; + import { Badge } from '$lib/components/ui/badge'; + import { CopiableText } from '$lib/components/copiable-text'; + import { MiddleEllipsis } from '$lib/components/middle-ellipsis'; + import { goto, invalidateAll } from '$app/navigation'; export let data: PageData; export let dialogOpen: boolean = false; @@ -47,36 +52,126 @@ } data.server.peers = data.server.peers.filter((peer) => peer.id !== peerId); }; + + const handleChangeState = async (state: string) => { + const resp = await fetchAction({ + action: '?/change-server-state', + method: 'POST', + form: { + state, + }, + }); + if (resp.statusText !== 'OK') { + console.error('err: failed to change server state.'); + return; + } + + console.log('server state changed!'); + if (state === 'remove') { + await goto('/'); + return; + } + + await invalidateAll(); + }; - + (dialogOpen = false)} /> -Hello there! + + + Server + + + + + + {data.server.name} + + + +
 {data.server.address}/24 
+
+ +
 {data.server.listen} 
+
+ + + + + + + + +
+ + + {#if data.server.status === 'up'} + + + {:else} + + + {/if} + +
Clients - - - {#each data.server.peers as peer} - { - handleRename(peer.id.toString(), detail); - }} - on:remove={() => { - handleRemove(peer.id.toString()); - }} - /> - {/each} - {#if data.server.peers.length > 0} + + {#each data.server.peers as peer} + { + handleRename(peer.id.toString(), detail); + }} + on:remove={() => { + handleRemove(peer.id.toString()); + }} + /> + {/each} + - + + {:else} + +
No Clients!
+ +
{/if}
diff --git a/web/src/routes/[serverId]/CreatePeerDialog.svelte b/web/src/routes/[serverId]/CreatePeerDialog.svelte index 5a1f55a..5904c83 100644 --- a/web/src/routes/[serverId]/CreatePeerDialog.svelte +++ b/web/src/routes/[serverId]/CreatePeerDialog.svelte @@ -1,13 +1,70 @@ -

Create Peer

-

Server Id: {serverId}

+ + Create Peer + + { + loading = true; + }, + onError: (e) => { + loading = false; + console.error('Client-side: FormError:', e); + }, + onResult: ({ result }) => { + if (result.type === 'success') { + handleSuccess(); + } else { + console.error('Server-failure: Result:', result); + } + loading = false; + }, + }} + > + + + Name + + + + + + + + + Create + + +
diff --git a/web/src/routes/[serverId]/DetailRow.svelte b/web/src/routes/[serverId]/DetailRow.svelte new file mode 100644 index 0000000..55cff84 --- /dev/null +++ b/web/src/routes/[serverId]/DetailRow.svelte @@ -0,0 +1,21 @@ + + +
+
+ {label} +
+
+ +
+
diff --git a/web/src/routes/[serverId]/schema.ts b/web/src/routes/[serverId]/schema.ts new file mode 100644 index 0000000..47085f6 --- /dev/null +++ b/web/src/routes/[serverId]/schema.ts @@ -0,0 +1,8 @@ +import { NameSchema } from '$lib/wireguard/schema'; +import { z } from 'zod'; + +export const CreatePeerSchema = z.object({ + name: NameSchema, +}); + +export type CreatePeerSchemaType = typeof CreatePeerSchema;