This commit is contained in:
Shahrad Elahi 2023-11-06 22:15:58 +03:30
parent 3fa6e3e63e
commit 2f2f0eac39
11 changed files with 444 additions and 25 deletions

View File

@ -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

View File

@ -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 };
},
};

View File

@ -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} />

View 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>

View 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>

View 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 };
},
};

View 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>

View 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>

View 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>

View 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' } });
};

View File

@ -9,3 +9,5 @@ export const CreateServerSchema = z.object({
dns: DnsSchema,
mtu: MtuSchema,
});
export type CreateServerSchemaType = typeof CreateServerSchema;