mirror of
https://github.com/wireadmin/wireadmin
synced 2025-03-09 13:20:39 +00:00
update
This commit is contained in:
parent
3fa6e3e63e
commit
2f2f0eac39
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="36px" height="36px" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- Generator: Sketch 62 (91390) - https://sketch.com -->
|
||||
<title>vps</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="vps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M33,26 L33,31 C33,32.6568542 31.6568542,34 30,34 L6,34 C4.34314575,34 3,32.6568542 3,31 L3,26 L33,26 Z M30,29 L6,29 L6,31 L30,31 L30,29 Z M33,15 L33,23 L3,23 L3,15 L33,15 Z M30,18 L6,18 L6,20 L30,20 L30,18 Z M30,4 C31.6568542,4 33,5.34314575 33,7 L33,12 L3,12 L3,7 C3,5.34314575 4.34314575,4 6,4 L30,4 Z M30,7 L6,7 L6,9 L30,9 L30,7 Z"
|
||||
fill="#991b1b"></path>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 751 B |
@ -1,12 +1,36 @@
|
||||
import type { Actions } from '@sveltejs/kit';
|
||||
import { type Actions, error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { findServer, getServers, WGServer } from '$lib/wireguard';
|
||||
import { superValidate } from 'sveltekit-superforms/server';
|
||||
import { CreateServerSchema } from './schema';
|
||||
import { NameSchema } from '$lib/wireguard/schema';
|
||||
|
||||
export const load: PageServerLoad = () => {
|
||||
return {};
|
||||
export const load: PageServerLoad = async () => {
|
||||
return {
|
||||
servers: (await getServers()).map((s) => s),
|
||||
form: superValidate(CreateServerSchema),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, cookies }) => {
|
||||
return { message: 'Success!' };
|
||||
rename: async ({ request, params }) => {
|
||||
const form = await request.formData();
|
||||
const serverId = (form.get('id') ?? '').toString();
|
||||
const name = (form.get('name') ?? '').toString();
|
||||
|
||||
const server = await findServer(serverId ?? '');
|
||||
if (!server) {
|
||||
console.error('Server not found');
|
||||
return error(404, 'Not found');
|
||||
}
|
||||
|
||||
if (!NameSchema.safeParse(name).success) {
|
||||
console.error('Peer name is invalid');
|
||||
return error(400, 'Bad Request');
|
||||
}
|
||||
|
||||
await WGServer.update(server.id, { name });
|
||||
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
||||
|
@ -1,20 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import BasePage from '$lib/components/page/BasePage.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import type { PageData } from './$types';
|
||||
import CreateServerDialog from './CreateServerDialog.svelte';
|
||||
import Server from './Server.svelte';
|
||||
import { Card, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import fetchAction from '$lib/fetch-action';
|
||||
|
||||
export let data: PageData;
|
||||
export let isOpen = false;
|
||||
|
||||
const handleRename = async (id: string, name: string) => {
|
||||
const resp = await fetchAction({
|
||||
action: '?/rename',
|
||||
method: 'POST',
|
||||
form: { id, name },
|
||||
});
|
||||
if (resp.statusText !== 'OK') {
|
||||
console.error('err: failed to rename server');
|
||||
return;
|
||||
}
|
||||
data.servers = data.servers.map((server) => {
|
||||
if (server.id === id) {
|
||||
server.name = name;
|
||||
}
|
||||
return server;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<BasePage showLogout={true}>
|
||||
<div class={'flex items-center justify-between py-3 px-2'}>
|
||||
<h2 class={'font-bold text-xl'}>Hello there 👋</h2>
|
||||
<Button on:click={() => (isOpen = true)}>
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Create Server
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content>
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
|
||||
<Button variant="outline">Button</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="space-y-3.5">
|
||||
{#if data.servers?.length < 1}
|
||||
<Card>No Servers Found</Card>
|
||||
{:else}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Servers</CardTitle>
|
||||
</CardHeader>
|
||||
{#each data.servers as server}
|
||||
<Server
|
||||
{server}
|
||||
on:rename={({ detail }) => {
|
||||
handleRename(server.id.toString(), detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</Card>
|
||||
{/if}
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<CreateServerDialog {isOpen} />
|
||||
|
61
web/src/routes/Server.svelte
Normal file
61
web/src/routes/Server.svelte
Normal file
@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import type { WgServer } from '$lib/typings';
|
||||
import { EditableText } from '$lib/components/editable-text';
|
||||
import { CopiableText } from '$lib/components/copiable-text';
|
||||
import { NameSchema } from '$lib/wireguard/schema';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
export let server: WgServer;
|
||||
export let addressPort: string = `${server.address}:${server.listen}`;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between p-4 gap-x-4">
|
||||
<div class="w-full md:w-2/3 flex items-center gap-x-2">
|
||||
<div class="flex grow">
|
||||
<div class={'w-12 aspect-square flex items-center justify-center mr-4 rounded-full bg-gray-200 max-md:hidden'}>
|
||||
<i class={'fa-solid fa-server text-gray-400 text-xl'} />
|
||||
</div>
|
||||
|
||||
<div class="h-full flex flex-col justify-between col-span-4 gap-y-1.5">
|
||||
<EditableText
|
||||
value={server.name}
|
||||
schema={NameSchema}
|
||||
rootClass="font-medium"
|
||||
inputClass="w-full max-w-[120px]"
|
||||
on:change={({ detail }) => {
|
||||
dispatch('rename', detail.value.toString());
|
||||
}}
|
||||
let:editMode
|
||||
asChild
|
||||
>
|
||||
<a href={`/${server.id}`} title="Manage the Server" class={cn({ hidden: editMode })}>
|
||||
<span class="text-lg md:text-base hover:text-primary hover:font-medium"> {server.name} </span>
|
||||
</a>
|
||||
</EditableText>
|
||||
<CopiableText value={addressPort} class="text-sm" showInHover={true}>
|
||||
<span class={'font-mono text-gray-400 text-xs'}> {addressPort} </span>
|
||||
</CopiableText>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href={`/${server.id}`} title="Manage the Server" class="hidden md:block">
|
||||
<Button variant="ghost" class="px-3 text-sm">Manage</Button>
|
||||
</a>
|
||||
</div>
|
19
web/src/routes/[serverId]/+layout.svelte
Normal file
19
web/src/routes/[serverId]/+layout.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import BasePage from '$lib/components/page/BasePage.svelte';
|
||||
</script>
|
||||
|
||||
<BasePage showLogout={true}>
|
||||
<div class="py-3 px-2">
|
||||
<a href="/" title="Home">
|
||||
<Button variant="outline" class="leading-none px-3 space-x-2.5">
|
||||
<i class="far fa-arrow-left"></i>
|
||||
<span> Back to Home </span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3.5">
|
||||
<slot />
|
||||
</div>
|
||||
</BasePage>
|
46
web/src/routes/[serverId]/+page.server.ts
Normal file
46
web/src/routes/[serverId]/+page.server.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { type Actions, error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { findServer, WGServer } from '$lib/wireguard';
|
||||
import { NameSchema } from '$lib/wireguard/schema';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const { serverId } = params;
|
||||
const server = await findServer(serverId);
|
||||
|
||||
if (server) {
|
||||
return { server };
|
||||
}
|
||||
|
||||
throw error(404, 'Not found');
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const name = (form.get('name') ?? '').toString();
|
||||
if (!NameSchema.safeParse(name).success) {
|
||||
console.error('Peer name is invalid');
|
||||
return error(400, 'Bad Request');
|
||||
}
|
||||
|
||||
await WGServer.updatePeer(server.id, peer.publicKey, { name });
|
||||
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
82
web/src/routes/[serverId]/+page.svelte
Normal file
82
web/src/routes/[serverId]/+page.svelte
Normal file
@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import CreatePeerDialog from './CreatePeerDialog.svelte';
|
||||
import { CardContent, CardFooter, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Peer from './Peer.svelte';
|
||||
import fetchAction from '$lib/fetch-action';
|
||||
|
||||
export let data: PageData;
|
||||
export let dialogOpen: boolean = false;
|
||||
|
||||
const handleRename = async (peerId: string, name: string) => {
|
||||
const resp = await fetchAction({
|
||||
action: '?/rename',
|
||||
method: 'POST',
|
||||
form: {
|
||||
id: peerId,
|
||||
name,
|
||||
},
|
||||
});
|
||||
if (resp.statusText !== 'OK') {
|
||||
console.error('err: failed to change peer name.');
|
||||
return;
|
||||
}
|
||||
data.server.peers = data.server.peers.map((peer) => {
|
||||
if (peer.id === peerId) {
|
||||
peer.name = name;
|
||||
}
|
||||
return peer;
|
||||
});
|
||||
console.info('peer name changed!');
|
||||
};
|
||||
|
||||
const handleRemove = async (peerId: string) => {
|
||||
const form = new FormData();
|
||||
form.set('id', peerId);
|
||||
|
||||
const resp = await fetchAction({
|
||||
action: '?/remove',
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
if (resp.statusText !== 'OK') {
|
||||
console.error('err: failed to remove peer.');
|
||||
return;
|
||||
}
|
||||
data.server.peers = data.server.peers.filter((peer) => peer.id !== peerId);
|
||||
};
|
||||
</script>
|
||||
|
||||
<CreatePeerDialog serverId={data.server.id} open={dialogOpen} />
|
||||
|
||||
<Card>Hello there!</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}
|
||||
<CardFooter>
|
||||
<Button class="btn btn-primary" on:click={() => (dialogOpen = true)}>Add Client</Button>
|
||||
</CardFooter>
|
||||
{/if}
|
||||
</Card>
|
97
web/src/routes/[serverId]/Peer.svelte
Normal file
97
web/src/routes/[serverId]/Peer.svelte
Normal file
@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import type { Peer } from '$lib/typings';
|
||||
import { CopiableText } from '$lib/components/copiable-text';
|
||||
import { EditableText } from '$lib/components/editable-text';
|
||||
import { NameSchema } from '$lib/wireguard/schema';
|
||||
import PeerActionButton from './PeerActionButton.svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { getPeerConf } from '$lib/wireguard/utils';
|
||||
|
||||
export let peer: Peer;
|
||||
|
||||
export let serverKey: string;
|
||||
export let serverPort: number;
|
||||
export let serverDNS: string | null;
|
||||
|
||||
export let conf: string | undefined = undefined;
|
||||
|
||||
export let isLoading: boolean = false;
|
||||
|
||||
onMount(async () => {
|
||||
conf = await getPeerConf({
|
||||
...peer,
|
||||
serverPublicKey: serverKey,
|
||||
port: serverPort,
|
||||
dns: serverDNS,
|
||||
});
|
||||
});
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!conf) {
|
||||
console.error('conf is null');
|
||||
return;
|
||||
}
|
||||
console.log('conf', conf);
|
||||
// create a blob
|
||||
const blob = new Blob([conf], { type: 'text/plain' });
|
||||
// create a link
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = `${peer.name}.conf`;
|
||||
// click the link
|
||||
link.click();
|
||||
// remove the link
|
||||
link.remove();
|
||||
};
|
||||
|
||||
const handleQRCode = async () => {};
|
||||
|
||||
const handleRename = (value: string) => {
|
||||
dispatch('rename', value);
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
dispatch('remove');
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between p-4 border border-neutral-200/60 rounded-md hover:border-neutral-200">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class={'w-12 aspect-square flex items-center justify-center mr-4 rounded-full bg-gray-200 max-md:hidden'}>
|
||||
<i class={'fas fa-user text-gray-400 text-lg'} />
|
||||
</div>
|
||||
|
||||
<div class="h-full flex flex-col justify-between col-span-4 gap-y-1.5">
|
||||
<EditableText
|
||||
rootClass={'font-medium col-span-4'}
|
||||
inputClass={'w-20'}
|
||||
schema={NameSchema}
|
||||
value={peer.name}
|
||||
on:change={({ detail }) => {
|
||||
handleRename(detail.value);
|
||||
}}
|
||||
/>
|
||||
<CopiableText value={peer.allowedIps} class={'text-sm'} showInHover={true}>
|
||||
<span class={'font-mono text-gray-400 text-xs'}> {peer.allowedIps} </span>
|
||||
</CopiableText>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-x-3">
|
||||
<!-- QRCode -->
|
||||
<PeerActionButton disabled={isLoading} on:click={handleQRCode}>
|
||||
<i class={'fal text-neutral-700 group-hover:text-primary fa-qrcode'} />
|
||||
</PeerActionButton>
|
||||
|
||||
<!-- Download -->
|
||||
<PeerActionButton disabled={isLoading} on:click={handleDownload}>
|
||||
<i class={'fal text-neutral-700 group-hover:text-primary fa-download'} />
|
||||
</PeerActionButton>
|
||||
|
||||
<!-- Download -->
|
||||
<PeerActionButton loading={isLoading} on:click={handleRemove}>
|
||||
<i class={'fal text-neutral-700 group-hover:text-primary text-lg fa-trash-can'} />
|
||||
</PeerActionButton>
|
||||
</div>
|
||||
</div>
|
35
web/src/routes/[serverId]/PeerActionButton.svelte
Normal file
35
web/src/routes/[serverId]/PeerActionButton.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let disabled: boolean = false;
|
||||
export let loading: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleClick() {
|
||||
if (disabled || loading) return;
|
||||
dispatch('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
aria-roledescription="Action"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class={cn(
|
||||
'group flex items-center justify-center w-10 aspect-square rounded-md',
|
||||
'bg-gray-200/80 hover:bg-gray-100/50',
|
||||
'border border-transparent hover:border-primary',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
'cursor-pointer',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
loading && 'animate-pulse',
|
||||
)}
|
||||
on:click={handleClick}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') handleClick();
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
22
web/src/routes/api/host/+server.ts
Normal file
22
web/src/routes/api/host/+server.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { WG_HOST } from '$env/static/private';
|
||||
import Shell from '$lib/shell';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
|
||||
let HOSTNAME = WG_HOST;
|
||||
|
||||
// if the host is not set, then we are using the server's public IP
|
||||
if (!HOSTNAME) {
|
||||
const resp = await Shell.exec('curl -s ifconfig.me', true);
|
||||
HOSTNAME = resp.trim();
|
||||
}
|
||||
|
||||
// check if WG_HOST is still not set
|
||||
if (!HOSTNAME) {
|
||||
console.error('WG_HOST is not set');
|
||||
return new Response('NOT_SET', { status: 500, headers: { 'Content-Type': 'text/plain' } });
|
||||
}
|
||||
|
||||
return new Response(HOSTNAME, { status: 200, headers: { 'Content-Type': 'text/plain' } });
|
||||
};
|
@ -9,3 +9,5 @@ export const CreateServerSchema = z.object({
|
||||
dns: DnsSchema,
|
||||
mtu: MtuSchema,
|
||||
});
|
||||
|
||||
export type CreateServerSchemaType = typeof CreateServerSchema;
|
Loading…
Reference in New Issue
Block a user