mirror of
https://github.com/wireadmin/wireadmin
synced 2025-06-26 18:28:06 +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 type Peer = z.infer<typeof PeerSchema>;
|
||||||
|
|
||||||
|
export const WgServerStatusSchema = z.enum(['up', 'down']);
|
||||||
|
|
||||||
export const WgServerSchema = z
|
export const WgServerSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
@ -58,7 +60,7 @@ export const WgServerSchema = z
|
|||||||
peers: z.array(PeerSchema),
|
peers: z.array(PeerSchema),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.string().datetime(),
|
||||||
status: z.enum(['up', 'down']),
|
status: WgServerStatusSchema,
|
||||||
})
|
})
|
||||||
.merge(WgKeySchema.omit({ preSharedKey: true }));
|
.merge(WgKeySchema.omit({ preSharedKey: true }));
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import deepmerge from 'deepmerge';
|
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 type { Peer, WgKey, WgServer } from '$lib/typings';
|
||||||
import Network from '$lib/network';
|
import Network from '$lib/network';
|
||||||
import Shell from '$lib/shell';
|
import Shell from '$lib/shell';
|
||||||
@ -57,7 +58,9 @@ export class WGServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.stop(id);
|
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);
|
const index = await findServerIndex(id);
|
||||||
if (typeof index !== 'number') {
|
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 confPath = path.join(WG_PATH, `wg${confId}.conf`);
|
||||||
const conf = fs.readFileSync(confPath, 'utf-8');
|
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> {
|
export async function writeConfigFile(wg: WgServer): Promise<void> {
|
||||||
|
@ -71,7 +71,8 @@ export const actions: Actions = {
|
|||||||
serverId,
|
serverId,
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} 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';
|
} from '$lib/components/ui/form';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { FormItem } from '$lib/components/ui/form/index.js';
|
import { FormItem } from '$lib/components/ui/form/index.js';
|
||||||
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
|
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils';
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
@ -30,7 +29,6 @@
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Server</DialogTitle>
|
<DialogTitle>Create Server</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<SuperDebug data={form} />
|
|
||||||
<Form
|
<Form
|
||||||
{form}
|
{form}
|
||||||
schema={CreateServerSchema}
|
schema={CreateServerSchema}
|
||||||
@ -43,6 +41,7 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
|
loading = false;
|
||||||
console.error('Client-side: FormError:', e);
|
console.error('Client-side: FormError:', e);
|
||||||
},
|
},
|
||||||
onResult: ({ result }) => {
|
onResult: ({ result }) => {
|
||||||
|
@ -43,15 +43,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={'flex col-span-4 justify-end'}>
|
<div class={'flex col-span-4 justify-end'}>
|
||||||
<Badge variant={server.status === 'up' ? 'success' : 'destructive'}>
|
{#if server.status === 'up'}
|
||||||
{#if server.status === 'up'}
|
<Badge variant="success">Online</Badge>
|
||||||
<span class="hidden md:inline">Online</span>
|
{:else}
|
||||||
<span class="inline md:hidden">Up</span>
|
<Badge variant="destructive">Offline</Badge>
|
||||||
{:else}
|
{/if}
|
||||||
<span class="hidden md:inline">Offline</span>
|
|
||||||
<span class="inline md:hidden">Down</span>
|
|
||||||
{/if}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { type Actions, error } from '@sveltejs/kit';
|
import { type Actions, error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
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 { 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 }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
const { serverId } = params;
|
const { serverId } = params;
|
||||||
@ -17,30 +20,156 @@ export const load: PageServerLoad = async ({ params }) => {
|
|||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
rename: async ({ request, params }) => {
|
rename: async ({ request, params }) => {
|
||||||
const { serverId } = params;
|
const { serverId } = params;
|
||||||
|
|
||||||
const server = await findServer(serverId ?? '');
|
const server = await findServer(serverId ?? '');
|
||||||
if (!server) {
|
if (!server) {
|
||||||
console.error('Server not found');
|
console.error('Server not found');
|
||||||
return error(404, 'Not found');
|
throw error(404, 'Not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
const peerId = (form.get('id') ?? '').toString();
|
const peerId = (form.get('id') ?? '').toString();
|
||||||
const peer = server.peers.find((p) => p.id === peerId);
|
const peer = server.peers.find((p) => p.id === peerId);
|
||||||
if (!peer) {
|
if (!peer) {
|
||||||
console.error('Peer not found');
|
console.error('Peer not found');
|
||||||
return error(404, 'Not found');
|
throw error(404, 'Not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = (form.get('name') ?? '').toString();
|
const name = (form.get('name') ?? '').toString();
|
||||||
if (!NameSchema.safeParse(name).success) {
|
if (!NameSchema.safeParse(name).success) {
|
||||||
console.error('Peer name is invalid');
|
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 { Button } from '$lib/components/ui/button';
|
||||||
import Peer from './Peer.svelte';
|
import Peer from './Peer.svelte';
|
||||||
import fetchAction from '$lib/fetch-action';
|
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 data: PageData;
|
||||||
export let dialogOpen: boolean = false;
|
export let dialogOpen: boolean = false;
|
||||||
@ -47,36 +52,126 @@
|
|||||||
}
|
}
|
||||||
data.server.peers = data.server.peers.filter((peer) => peer.id !== peerId);
|
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>
|
</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>
|
<Card>
|
||||||
<CardHeader class="flex flex-row items-center justify-between">
|
<CardHeader class="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Clients</CardTitle>
|
<CardTitle>Clients</CardTitle>
|
||||||
<span> </span>
|
|
||||||
</CardHeader>
|
</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}
|
{#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>
|
<CardFooter>
|
||||||
<Button class="btn btn-primary" on:click={() => (dialogOpen = true)}>Add Client</Button>
|
<Button on:click={() => (dialogOpen = true)}>Add Client</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
{:else}
|
||||||
|
<CardContent>
|
||||||
|
<div>No Clients!</div>
|
||||||
|
<Button on:click={() => (dialogOpen = true)}>Add Client</Button>
|
||||||
|
</CardContent>
|
||||||
{/if}
|
{/if}
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,13 +1,70 @@
|
|||||||
<script lang="ts">
|
<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;
|
const dispatch = createEventDispatcher();
|
||||||
export let open: boolean = false;
|
|
||||||
|
export let open = false;
|
||||||
|
let loading: boolean = false;
|
||||||
|
|
||||||
|
let form: SuperValidated<CreatePeerSchemaType>;
|
||||||
|
|
||||||
|
const handleSuccess = async () => {
|
||||||
|
await invalidateAll();
|
||||||
|
dispatch('close', 'OK');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog {open}>
|
<Dialog {open}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<h2>Create Peer</h2>
|
<DialogHeader>
|
||||||
<p>Server Id: {serverId}</p>
|
<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>
|
</DialogContent>
|
||||||
</Dialog>
|
</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