mirror of
https://github.com/wireadmin/wireadmin
synced 2025-02-26 05:48:44 +00:00
update
This commit is contained in:
parent
98ea76be1a
commit
034465d17e
@ -41,6 +41,8 @@ export const PeerSchema = z
|
||||
|
||||
export type Peer = z.infer<typeof PeerSchema>;
|
||||
|
||||
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 }));
|
||||
|
||||
|
@ -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<void> {
|
||||
|
@ -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');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -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 @@
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Server</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SuperDebug data={form} />
|
||||
<Form
|
||||
{form}
|
||||
schema={CreateServerSchema}
|
||||
@ -43,6 +41,7 @@
|
||||
loading = true;
|
||||
},
|
||||
onError: (e) => {
|
||||
loading = false;
|
||||
console.error('Client-side: FormError:', e);
|
||||
},
|
||||
onResult: ({ result }) => {
|
||||
|
@ -43,15 +43,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class={'flex col-span-4 justify-end'}>
|
||||
<Badge variant={server.status === 'up' ? 'success' : 'destructive'}>
|
||||
{#if server.status === 'up'}
|
||||
<span class="hidden md:inline">Online</span>
|
||||
<span class="inline md:hidden">Up</span>
|
||||
{:else}
|
||||
<span class="hidden md:inline">Offline</span>
|
||||
<span class="inline md:hidden">Down</span>
|
||||
{/if}
|
||||
</Badge>
|
||||
{#if server.status === 'up'}
|
||||
<Badge variant="success">Online</Badge>
|
||||
{:else}
|
||||
<Badge variant="destructive">Offline</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -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();
|
||||
};
|
||||
</script>
|
||||
|
||||
<CreatePeerDialog serverId={data.server.id} open={dialogOpen} />
|
||||
<CreatePeerDialog open={dialogOpen} on:close={() => (dialogOpen = false)} />
|
||||
|
||||
<Card>Hello there!</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Server</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<DetailRow label={'Name'}>
|
||||
<CopiableText value={data.server.name}>
|
||||
{data.server.name}
|
||||
</CopiableText>
|
||||
</DetailRow>
|
||||
<DetailRow label={'IP address'}>
|
||||
<pre> {data.server.address}/24 </pre>
|
||||
</DetailRow>
|
||||
<DetailRow label={'Listen Port'}>
|
||||
<pre> {data.server.listen} </pre>
|
||||
</DetailRow>
|
||||
<DetailRow label={'Status'}>
|
||||
<Badge variant={data.server.status === 'up' ? 'success' : 'destructive'} />
|
||||
</DetailRow>
|
||||
<DetailRow label={'Public Key'}>
|
||||
<CopiableText value={data.server.publicKey}>
|
||||
<MiddleEllipsis content={data.server.publicKey} maxLength={16} />
|
||||
</CopiableText>
|
||||
</DetailRow>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter class="flex flex-wrap items-center gap-2">
|
||||
{#if data.server.status === 'up'}
|
||||
<Button
|
||||
variant="outline"
|
||||
class="max-md:w-full"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
handleChangeState('restart');
|
||||
}}>Restart</Button
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
class="max-md:w-full"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
handleChangeState('stop');
|
||||
}}>Stop</Button
|
||||
>
|
||||
{:else}
|
||||
<Button
|
||||
variant="success"
|
||||
class="max-md:w-full bg-green-500"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
handleChangeState('start');
|
||||
}}>Start</Button
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
class="max-md:w-full"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
handleChangeState('remove');
|
||||
}}>Remove</Button
|
||||
>
|
||||
{/if}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between">
|
||||
<CardTitle>Clients</CardTitle>
|
||||
<span> </span>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#each data.server.peers as peer}
|
||||
<Peer
|
||||
{peer}
|
||||
serverKey={data.server.publicKey}
|
||||
serverPort={data.server.listen}
|
||||
serverDNS={data.server.dns}
|
||||
on:rename={({ detail }) => {
|
||||
handleRename(peer.id.toString(), detail);
|
||||
}}
|
||||
on:remove={() => {
|
||||
handleRemove(peer.id.toString());
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</CardContent>
|
||||
{#if data.server.peers.length > 0}
|
||||
<CardContent class="space-y-3">
|
||||
{#each data.server.peers as peer}
|
||||
<Peer
|
||||
{peer}
|
||||
serverKey={data.server.publicKey}
|
||||
serverPort={data.server.listen}
|
||||
serverDNS={data.server.dns}
|
||||
on:rename={({ detail }) => {
|
||||
handleRename(peer.id.toString(), detail);
|
||||
}}
|
||||
on:remove={() => {
|
||||
handleRemove(peer.id.toString());
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button class="btn btn-primary" on:click={() => (dialogOpen = true)}>Add Client</Button>
|
||||
<Button on:click={() => (dialogOpen = true)}>Add Client</Button>
|
||||
</CardFooter>
|
||||
{:else}
|
||||
<CardContent>
|
||||
<div>No Clients!</div>
|
||||
<Button on:click={() => (dialogOpen = true)}>Add Client</Button>
|
||||
</CardContent>
|
||||
{/if}
|
||||
</Card>
|
||||
|
@ -1,13 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { Dialog, DialogContent } from '$lib/components/ui/dialog';
|
||||
import { CreatePeerSchema, type CreatePeerSchemaType } from './schema';
|
||||
import type { SuperValidated } from 'sveltekit-superforms';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '$lib/components/ui/dialog';
|
||||
import { Form, FormButton, FormField, FormInput, FormLabel, FormValidation } from '$lib/components/ui/form';
|
||||
import { FormItem } from '$lib/components/ui/form/index.js';
|
||||
import { cn } from '$lib/utils';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let serverId: string;
|
||||
export let open: boolean = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let open = false;
|
||||
let loading: boolean = false;
|
||||
|
||||
let form: SuperValidated<CreatePeerSchemaType>;
|
||||
|
||||
const handleSuccess = async () => {
|
||||
await invalidateAll();
|
||||
dispatch('close', 'OK');
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dialog {open}>
|
||||
<DialogContent>
|
||||
<h2>Create Peer</h2>
|
||||
<p>Server Id: {serverId}</p>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Peer</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form
|
||||
{form}
|
||||
schema={CreatePeerSchema}
|
||||
class="space-y-3.5"
|
||||
action="?/create"
|
||||
method={'POST'}
|
||||
let:config
|
||||
options={{
|
||||
onSubmit: (s) => {
|
||||
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;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormField {config} name={'name'}>
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormInput placeholder={'e.g. Unicorn'} type={'text'} />
|
||||
<FormValidation />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<DialogFooter>
|
||||
<FormButton>
|
||||
<i class={cn(loading ? 'far fa-arrow-rotate-right animate-spin' : 'far fa-plus', 'mr-2')}></i>
|
||||
Create
|
||||
</FormButton>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
21
web/src/routes/[serverId]/DetailRow.svelte
Normal file
21
web/src/routes/[serverId]/DetailRow.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let label: string;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'flex flex-wrap items-center justify-between gap-2 py-3.5',
|
||||
'relative overflow-ellipsis',
|
||||
'leading-none',
|
||||
'border-b border-gray-200/80 last:border-none',
|
||||
)}
|
||||
>
|
||||
<div class={'flex items-center text-gray-400 text-sm col-span-12 md:col-span-3'}>
|
||||
{label}
|
||||
</div>
|
||||
<div class={'flex items-center gap-x-2 col-span-12 md:col-span-9'}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
8
web/src/routes/[serverId]/schema.ts
Normal file
8
web/src/routes/[serverId]/schema.ts
Normal file
@ -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;
|
Loading…
Reference in New Issue
Block a user