This commit is contained in:
Shahrad Elahi
2024-05-29 16:23:25 +03:30
parent 9ee534f2bb
commit ae787625b9
170 changed files with 2434 additions and 2643 deletions

View File

@@ -74,6 +74,9 @@
}
body {
@apply bg-background text-foreground;
font-feature-settings:
'rlig' 1,
'calt' 1;
}
}

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<link rel="stylesheet" href="%sveltekit.assets%/fontawesome/all.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>

View File

@@ -1,25 +1,25 @@
import { type Handle, redirect } from '@sveltejs/kit';
import { verifyToken } from '$lib/auth';
import { AUTH_COOKIE } from '$lib/constants';
import 'dotenv/config';
import { redirect, type Handle } from '@sveltejs/kit';
import { verifyToken } from '@lib/auth';
import { AUTH_COOKIE } from '@lib/constants';
import logger from '@lib/logger';
export const handle: Handle = async ({ event, resolve }) => {
if (!AUTH_EXCEPTION.includes(event.url.pathname)) {
const token = event.cookies.get(AUTH_COOKIE);
const token_valid = await verifyToken(token ?? '');
logger.debug(`-> ${event.request.method} ${event.url.pathname}`);
const is_login_page = event.url.pathname === '/login';
if (!token_valid && !is_login_page) {
// return redirect;
throw redirect(303, '/login');
}
const token = event.cookies.get(AUTH_COOKIE);
const token_valid = await verifyToken(token ?? '');
if (token_valid && is_login_page) {
throw redirect(303, '/');
}
const is_login_page = event.url.pathname === '/login';
if (!token_valid && !is_login_page) {
throw redirect(303, '/login');
}
if (token_valid && is_login_page) {
throw redirect(303, '/');
}
return resolve(event);
};
const AUTH_EXCEPTION = ['/api/health'];

View File

@@ -1,34 +1,42 @@
import jwt from 'jsonwebtoken';
import { client } from '$lib/storage';
import { env } from '$lib/env';
export async function generateToken(): Promise<string> {
import { WG_AUTH_PATH } from '@lib/constants';
import { env } from '@lib/env';
import { storage } from '@lib/storage';
import { sha256 } from '@lib/utils/hash';
interface GenerateTokenParams {
expiresIn: number;
}
export async function generateToken(params: GenerateTokenParams): Promise<string> {
const now = Math.floor(Date.now() / 1000);
const oneHour = 60 * 60;
const token = jwt.sign(
{
ok: true,
iat: now,
exp: now + oneHour,
exp: now + params.expiresIn,
},
env.AUTH_SECRET,
env.AUTH_SECRET
);
client.setex(token, '1', oneHour);
await storage.lpushex(WG_AUTH_PATH, sha256(token), params.expiresIn);
return token;
}
export async function verifyToken(token: string): Promise<boolean> {
try {
const decode = jwt.verify(token, env.AUTH_SECRET);
if (!decode) return false;
if (!token || !(await storage.lexists(WG_AUTH_PATH, sha256(token)))) return false;
const exists = client.exists(token);
return exists;
return !!jwt.verify(token, env.AUTH_SECRET);
} catch (e) {
return false;
}
}
export async function revokeToken(token: string): Promise<void> {
client.del(token);
if (!token) return;
const index = await storage
.lgetall(WG_AUTH_PATH)
.then((l) => l.findIndex((t) => t === sha256(token)));
await storage.ldel(WG_AUTH_PATH, index);
}

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { ClipboardCopyIcon } from 'lucide-svelte';
import { Button } from '@lib/components/ui/button';
export let showInHover: boolean = false;
export let rootClass: string | undefined = undefined;
@@ -12,20 +14,23 @@
};
</script>
<div class={cn('group flex items-center', rootClass)}>
<div class={cn('group flex items-center gap-3', rootClass)}>
<slot />
<i
<Button
aria-roledescription="Copy to clipboard"
role="button"
tabindex="0"
class={cn(
'ml-2 mb-0.5 far fa-copy cursor-pointer text-gray-400/80 hover:text-primary',
showInHover && 'group-hover:opacity-100 opacity-0',
className,
)}
size="none"
variant="ghost"
on:click={handleCopy}
on:keydown={(e) => {
if (e.key === 'Enter') handleCopy();
}}
/>
>
<ClipboardCopyIcon
class={cn(
'h-4 w-4 cursor-pointer text-gray-400/80 hover:text-primary',
showInHover && 'group-hover:opacity-100 opacity-0',
className
)}
/>
</Button>
</div>

View File

@@ -1,7 +1,9 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { createEventDispatcher } from 'svelte';
import type { ZodEffects, ZodString } from 'zod';
import { SquarePenIcon } from 'lucide-svelte';
import { Button } from '@lib/components/ui/button';
export let editMode: boolean = false;
export let schema: ZodString | ZodEffects<any>;
@@ -40,7 +42,7 @@
editMode ? 'block' : 'hidden',
'w-full ring-2 ring-neutral-800 ring-offset-2 rounded transition-colors duration-200 ease-in-out outline-transparent',
inputClass,
error && 'ring-red-500 rounded',
error && 'ring-red-500 rounded'
)}
{value}
on:keydown={(e) => {
@@ -60,14 +62,16 @@
}}
/>
<i
class="fal fa-pen-to-square text-sm opacity-0 group-hover:opacity-100 text-neutral-400 hover:text-primary cursor-pointer"
role="button"
tabindex="0"
<Button
class="opacity-0 group-hover:opacity-100 text-gray-400/80 group-hover:text-primary"
aria-roledescription="Edit"
size="none"
variant="ghost"
on:click={handleEnterEditMode}
on:keydown={(e) => {
if (e.key === 'Enter') handleEnterEditMode();
}}
/>
>
<SquarePenIcon class="h-4 w-4" />
</Button>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { mode } from 'mode-watcher';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<SVGImageElement> & {
borderColor?: string;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { EmptyDescription, EmptySimpleImage, type Props } from '.';
type $$Props = Props;

View File

@@ -1,7 +1,8 @@
import Root from './empty.svelte';
import type { HTMLAttributes } from 'svelte/elements';
import Description from './empty-description.svelte';
import SimpleImage from './empty-simple-img.svelte';
import type { HTMLAttributes } from 'svelte/elements';
import Root from './empty.svelte';
interface Props extends HTMLAttributes<HTMLDivElement> {
description?: string | null;

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn } from '@lib/utils.js';
export let name: string | undefined = undefined;
export let color = 'currentColor';
export let size: number | string = 24;
export let strokeWidth: number | string = 2;
export let absoluteStrokeWidth: boolean = false;
</script>
<svg
{...$$restProps}
width={size}
height={size}
stroke={color}
stroke-width={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}
class={cn('lucide-icon', 'lucide', name ? `lucide-${name}` : '', $$props.class)}
>
<slot />
</svg>

View File

@@ -0,0 +1,21 @@
import type { SVGAttributes } from 'svelte/elements';
import Root from './icon.svelte';
import Onion from './onion-icon.svelte';
interface Props extends SVGAttributes<SVGSVGElement> {
color?: string;
size?: number | string;
strokeWidth?: number | string;
absoluteStrokeWidth?: boolean;
class?: string;
}
export {
Root,
Onion,
type Props,
//
Root as Icon,
Onion as OnionIcon,
};

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import Icon from '@lib/components/iconset/icon.svelte';
import type { Props } from '@lib/components/iconset';
type $$Props = Props;
</script>
<Icon {...$$restProps} viewBox="0 0 179.51151 205.15602" fill="currentColor">
<g transform="translate(-17.934981,-39.932967)">
<path
d="m 104.88587,47.145484 -1.60279,-2.804867 q -2.00347,-4.407649 -6.811816,-4.407649 -7.212516,0.801391 -8.414602,8.013907 -2.804867,14.024336 -11.620164,24.041719 -8.815297,9.616688 -20.435462,18.431985 0,0 0,0 0,0 0,0 -14.425031,10.818771 -26.045196,25.644501 -11.219469,14.42503 -12.02086,39.26814 0.400696,24.04172 15.226423,40.47023 14.825727,16.02781 37.66536,23.64102 l -4.407648,12.82225 q -2.003477,8.8153 6.010429,12.42156 8.815298,2.00348 12.421555,-6.01043 l 4.808344,-14.42503 q 4.006954,0.80139 8.414602,1.20208 v 10.01739 q 0.801391,8.81529 9.616685,9.61668 8.8153,-0.80139 9.61669,-9.61668 V 225.4549 q 4.40765,-0.40069 8.4146,-1.20208 l 4.80835,14.42503 q 3.60625,8.01391 12.42155,6.01043 8.01391,-3.60626 6.01043,-12.42156 l -4.40765,-12.82225 q 22.83964,-7.61321 37.66536,-23.64102 14.82573,-16.42851 15.22643,-40.47023 -0.8014,-24.84311 -12.02086,-39.26814 -11.62017,-14.82573 -26.0452,-25.644501 0,0 0,0 0,0 0,0 -11.62016,-8.815297 -20.43546,-18.431985 -8.8153,-10.017383 -11.21947,-24.041719 -1.60278,-7.212516 -8.8153,-8.013907 -4.80834,0 -6.81182,4.407649 l -1.60278,2.804867 q -2.80487,3.606258 -5.60973,0 z M 56.802427,114.4623 v -0.4007 q 0.400695,-0.40069 0.80139,-0.80139 0,0 0,0 1.602782,-2.40417 4.407649,-2.80486 1.202086,0 2.404172,0.40069 0.801391,0 2.003477,0.80139 2.404172,1.60278 2.804867,4.40765 0,1.20209 -0.400695,2.40417 0,0.80139 -0.400696,1.20209 v 0.40069 q -0.400695,0.4007 -0.80139,1.20209 -0.400696,1.60278 -2.003477,4.40765 -2.404172,6.01043 -5.209039,15.22642 -2.804868,9.61669 -3.606258,19.63407 -0.400696,10.01738 2.404172,18.03129 1.602781,6.01043 -4.006953,8.4146 -5.609735,1.60278 -8.013907,-3.60626 -4.006953,-11.62016 -3.205563,-23.64102 1.202086,-12.02086 4.006954,-22.03824 3.205562,-10.41808 6.01043,-16.82921 1.202086,-3.20556 2.003476,-4.80834 0.400695,-0.4007 0.400695,-0.80139 0,-0.4007 0.400696,-0.80139 z M 101.6803,89.218493 q 0.4007,-0.801391 0.4007,-1.202086 0.80139,-1.202086 2.00348,-2.003477 2.00347,-1.602781 4.80834,-1.202086 1.20208,0.400696 2.00348,0.801391 1.20208,0.801391 2.00347,2.003477 0.80139,1.202086 1.20209,2.804867 0,0.801391 0,2.003477 -0.4007,0.80139 -0.4007,1.202086 v 0 l -0.40069,0.80139 q 0,0 0,0.400696 -0.4007,0.80139 -0.80139,1.602781 0,0 0,0.400695 -1.20209,2.804867 -2.40418,7.212516 -3.60625,10.01738 -7.21251,25.6445 -4.006954,15.62712 -5.20904,32.05563 -0.801391,16.8292 3.20556,30.05215 1.60278,5.60973 -4.006951,8.0139 -5.609734,1.20209 -8.013906,-4.00695 -4.808344,-16.02781 -3.606258,-34.4598 1.202086,-18.43198 5.209039,-34.86049 3.606258,-16.02781 7.212516,-26.846589 2.003476,-4.808343 2.80487,-8.013906 0.80139,-1.202086 1.20208,-2.003477 v -0.400695 -0.400695 z m 31.65493,14.825727 q 0.4007,-6.01043 6.41113,-6.411125 0,0 0,0 0,0 0,0 0,0 0,0 0,0 0.40069,0 3.60626,0 5.60974,3.205565 l -0.4007,0.40069 v 0 q 0.4007,-0.40069 0.4007,-0.40069 v 0 0 0 0.40069 l 0.40069,0.4007 q 0.4007,0.40069 0.80139,1.60278 1.20209,2.00348 2.80487,5.60973 3.60626,7.61322 8.01391,19.63407 4.40765,12.02086 6.41112,26.0452 2.00348,14.42503 -1.60278,28.04867 -2.40417,5.60974 -8.0139,4.40765 -5.60974,-2.00347 -4.40765,-7.61321 2.80486,-10.81877 1.20208,-23.24033 -1.60278,-12.02086 -5.60973,-23.64102 -4.00695,-11.21947 -7.61321,-18.03129 -1.60278,-3.60626 -2.40417,-5.60974 v 0 q -2.40418,-1.60278 -2.40418,-4.80834 z"
aria-label="Onion"
/>
</g>
</Icon>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
export let content: string;
export let maxLength: number = Math.floor(content.length / 2);

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from '$lib/utils';
import PageFooter from '$lib/components/page/PageFooter.svelte';
import PageHeader from '$lib/components/page/PageHeader.svelte';
import { cn } from '@lib/utils';
import PageFooter from '@lib/components/page/PageFooter.svelte';
import PageHeader from '@lib/components/page/PageHeader.svelte';
export let rootClass: string | undefined = undefined;
let className: string | undefined = undefined;

View File

@@ -1,5 +1,5 @@
<script>
import DotDivider from '$lib/components/DotDivider.svelte';
import DotDivider from '@lib/components/DotDivider.svelte';
</script>
<footer class={'flex items-center justify-center'}>
@@ -12,7 +12,7 @@
</a>
<DotDivider className="font-bold text-gray-400" />
<a
href={'https://github.com/shahradelahi/wireadmin'}
href={'https://github.com/wireadmin/wireadmin'}
title={'Github'}
class={'px-2 font-medium text-gray-400/80 hover:text-gray-500 text-xs'}
>

View File

@@ -3,8 +3,8 @@
import Moon from 'lucide-svelte/icons/moon';
import { toggleMode } from 'mode-watcher';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils';
import { Button } from '@lib/components/ui/button';
import { LogOutIcon } from 'lucide-svelte';
export let showLogout: boolean = false;
</script>
@@ -18,12 +18,12 @@
<div class={'flex items-center gap-x-3'}>
<a
href={'https://github.com/shahradelahi/wireadmin'}
href={'https://github.com/wireadmin/wireadmin'}
title={'Giv me a star on Github'}
class="hidden md:block"
>
<img
src={'https://img.shields.io/github/stars/shahradelahi/wireadmin.svg?style=social&label=Star'}
src={'https://img.shields.io/github/stars/wireadmin/wireadmin.svg?style=social&label=Star'}
alt={'Gimme a Star'}
/>
</a>
@@ -41,17 +41,8 @@
{#if showLogout}
<a href="/logout" rel="external" title="Logout">
<Button variant="ghost" class="group text-sm/2 gap-x-2 font-medium">
<i
class={cn(
'far fa-arrow-right-from-arc text-sm mr-0.5',
'text-neutral-500 group-hover:text-neutral-800',
'dark:text-neutral-400 dark:group-hover:text-neutral-100',
)}
></i>
<span
class="text-neutral-700 hover:text-neutral-800 dark:text-neutral-100 dark:hover:text-neutral-100"
>Logout</span
>
<LogOutIcon class={'w-4 h-4 mr-0.5'} />
Logout
</Button>
</a>
{/if}

View File

@@ -6,11 +6,11 @@
DialogHeader,
DialogTitle,
DialogTrigger,
} from '$lib/components/ui/dialog';
import { Skeleton } from '$lib/components/ui/skeleton';
} from '@lib/components/ui/dialog';
import { Skeleton } from '@lib/components/ui/skeleton';
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Button } from '$lib/components/ui/button';
import type { SafeReturn } from '$lib/typings';
import { Button } from '@lib/components/ui/button';
import type { SafeReturn } from '@lib/typings';
const DialogClose = DialogPrimitive.Close;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { badgeVariants, type Variant } from '.';
let className: string | undefined | null = undefined;

View File

@@ -1,4 +1,5 @@
import { tv, type VariantProps } from 'tailwind-variants';
export { default as Badge } from './badge.svelte';
export const badgeVariants = tv({

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn('flex h-9 w-9 items-center justify-center', className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More</span>
</span>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLLiAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<li bind:this={el} class={cn('inline-flex items-center gap-1.5', className)}>
<slot />
</li>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
export let href: $$Props['href'] = undefined;
export let el: $$Props['el'] = undefined;
export let asChild: $$Props['asChild'] = false;
let className: $$Props['class'] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn('transition-colors hover:text-foreground', className),
href,
...$$restProps,
};
</script>
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</a>
{/if}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLOlAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<ol
bind:this={el}
class={cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className
)}
{...$$restProps}
>
<slot />
</ol>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props['el'] = undefined;
export let className: $$Props['class'] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn('font-normal text-foreground', className)}
{...$$restProps}
>
<slot />
</span>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import type { HTMLLiAttributes } from 'svelte/elements';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from '@lib/utils.js';
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<li
role="presentation"
aria-hidden="true"
class={cn('[&>svg]:size-3.5', className)}
bind:this={el}
{...$$restProps}
>
<slot>
<ChevronRight />
</slot>
</li>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props['el'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
</nav>

View File

@@ -0,0 +1,25 @@
import Ellipsis from './breadcrumb-ellipsis.svelte';
import Item from './breadcrumb-item.svelte';
import Link from './breadcrumb-link.svelte';
import List from './breadcrumb-list.svelte';
import Page from './breadcrumb-page.svelte';
import Separator from './breadcrumb-separator.svelte';
import Root from './breadcrumb.svelte';
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Button as ButtonPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { type Events, type Props, buttonVariants } from '.';
import { type Events, type Props, buttonVariants } from './index';
import { cn } from '@lib/utils';
type $$Props = Props;
type $$Events = Events;

View File

@@ -1,5 +1,6 @@
import { type VariantProps, tv } from 'tailwind-variants';
import type { Button as ButtonPrimitive } from 'bits-ui';
import { tv, type VariantProps } from 'tailwind-variants';
import Root from './button.svelte';
const buttonVariants = tv({
@@ -8,7 +9,6 @@ const buttonVariants = tv({
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
success: 'bg-green-500 text-white hover:bg-green-500/90 hover:text-gray-50',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
@@ -19,6 +19,7 @@ const buttonVariants = tv({
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
none: '',
},
},
defaultVariants: {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLParagraphElement>;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import type { HeadingLevel } from './index.js';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,9 +1,9 @@
import Root from './card.svelte';
import Content from './card-content.svelte';
import Description from './card-description.svelte';
import Footer from './card-footer.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';
import Root from './card.svelte';
export {
Root,

View File

@@ -2,7 +2,7 @@
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
import Check from 'lucide-svelte/icons/check';
import Minus from 'lucide-svelte/icons/minus';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
@@ -15,7 +15,7 @@
<CheckboxPrimitive.Root
class={cn(
'peer box-content h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[disabled=true]:opacity-50',
className,
className
)}
bind:checked
{...$$restProps}

View File

@@ -1,4 +1,5 @@
import Root from './checkbox.svelte';
export {
Root,
//

View File

@@ -1,4 +1,5 @@
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
import Content from './collapsible-content.svelte';
const Root = CollapsiblePrimitive.Root;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import * as Dialog from '.';
import { cn, flyAndScale } from '$lib/utils';
import { cn, flyAndScale } from '@lib/utils';
import { X } from 'lucide-svelte';
type $$Props = DialogPrimitive.ContentProps;
@@ -21,7 +21,7 @@
{transitionConfig}
class={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
className,
className
)}
{...$$restProps}
>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = DialogPrimitive.DescriptionProps;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import { fade } from 'svelte/transition';
type $$Props = DialogPrimitive.OverlayProps;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = DialogPrimitive.TitleProps;

View File

@@ -1,17 +1,17 @@
import { Dialog as DialogPrimitive } from 'bits-ui';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Portal from './dialog-portal.svelte';
import Title from './dialog-title.svelte';
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
import Title from './dialog-title.svelte';
import Portal from './dialog-portal.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
export {
Root,
Title,

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import * as Button from '$lib/components/ui/button/index.js';
import * as Button from '@lib/components/ui/button/index.js';
type $$Props = Button.Props;
type $$Events = Button.Events;

View File

@@ -1,27 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { Checkbox as CheckboxPrimitive } from 'bits-ui';
import { Checkbox } from '$lib/components/ui/checkbox';
type $$Props = CheckboxPrimitive.Props;
type $$Events = CheckboxPrimitive.Events;
export let onCheckedChange: $$Props['onCheckedChange'] = undefined;
const { name, setValue, attrStore, value } = getFormField();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name: nameAttr, value: valueAttr, ...rest } = $attrStore;
</script>
<Checkbox
{...rest}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v);
setValue(v);
}}
{...$$restProps}
on:click
on:keydown
/>
<input hidden {name} value={$value} />

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;

View File

@@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
@@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;

View File

@@ -1,6 +1,5 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
@@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from 'svelte/elements';
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;

View File

@@ -1,13 +1,12 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.FieldsetProps<T, U>;

View File

@@ -1,28 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { HTMLInputAttributes } from 'svelte/elements';
import { Input, type InputEvents } from '$lib/components/ui/input';
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
const { attrStore, value } = getFormField();
</script>
<Input
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
/>

View File

@@ -1,12 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<div class={cn('space-y-2', className)} {...$$restProps}>
<slot />
</div>

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils';
import { Label } from '$lib/components/ui/label';
import { cn } from '@lib/utils.js';
import { Label } from '@lib/components/ui/label/index.js';
type $$Props = LabelPrimitive.Props;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import * as FormPrimitive from 'formsnap';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = FormPrimitive.LegendProps;

View File

@@ -1,24 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap';
import { buttonVariants } from '$lib/components/ui/button';
import { cn } from '$lib/utils';
import { ChevronDown } from 'lucide-svelte';
import type { HTMLSelectAttributes } from 'svelte/elements';
type $$Props = HTMLSelectAttributes;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Select
class={cn(
buttonVariants({ variant: 'outline' }),
'appearance-none bg-transparent font-normal',
className,
)}
{...$$restProps}
>
<slot />
</FormPrimitive.Select>
<ChevronDown class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />

View File

@@ -1,22 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import * as RadioGroup from '$lib/components/ui/radio-group';
type $$Props = RadioGroupPrimitive.Props;
const { attrStore, setValue, name, value } = getFormField();
export let onValueChange: $$Props['onValueChange'] = undefined;
</script>
<RadioGroup.Root
{...$attrStore}
onValueChange={(v) => {
onValueChange?.(v);
setValue(v);
}}
{...$$restProps}
>
<slot />
<input hidden {name} value={$value} />
</RadioGroup.Root>

View File

@@ -1,17 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import type { Select as SelectPrimitive } from 'bits-ui';
import { getFormField } from 'formsnap';
type $$Props = SelectPrimitive.TriggerProps & {
placeholder?: string;
};
type $$Events = SelectPrimitive.TriggerEvents;
const { attrStore } = getFormField();
export let placeholder = '';
</script>
<Select.Trigger {...$$restProps} {...$attrStore} on:click on:keydown>
<Select.Value {placeholder} />
<slot />
</Select.Trigger>

View File

@@ -1,20 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import { getFormField } from 'formsnap';
import type { Select as SelectPrimitive } from 'bits-ui';
type $$Props = SelectPrimitive.Props;
const { setValue, name, value } = getFormField();
export let onSelectedChange: $$Props['onSelectedChange'] = undefined;
</script>
<Select.Root
onSelectedChange={(v) => {
onSelectedChange?.(v);
setValue(v ? v.value : undefined);
}}
{...$$restProps}
>
<slot />
<input hidden {name} value={$value} />
</Select.Root>

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { Switch as SwitchPrimitive } from 'bits-ui';
import { Switch } from '$lib/components/ui/switch';
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
export let onCheckedChange: $$Props['onCheckedChange'] = undefined;
const { name, setValue, attrStore, value } = getFormField();
</script>
<Switch
{...$attrStore}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v);
setValue(v);
}}
{...$$restProps}
on:click
on:keydown
/>
<input hidden {name} value={$value} />

View File

@@ -1,29 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap';
import type { HTMLTextareaAttributes } from 'svelte/elements';
import type { TextareaGetFormField } from './index';
import { Textarea, type TextareaEvents } from '$lib/components/ui/textarea';
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
const { attrStore, value } = getFormField() as TextareaGetFormField;
</script>
<Textarea
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
/>

View File

@@ -1,14 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap';
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLParagraphElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Validation
class={cn('text-sm font-medium text-destructive', className)}
{...$$restProps}
/>

View File

@@ -1,12 +1,13 @@
import * as FormPrimitive from 'formsnap';
import Button from './form-button.svelte';
import Description from './form-description.svelte';
import Label from './form-label.svelte';
import ElementField from './form-element-field.svelte';
import FieldErrors from './form-field-errors.svelte';
import Field from './form-field.svelte';
import Fieldset from './form-fieldset.svelte';
import Label from './form-label.svelte';
import Legend from './form-legend.svelte';
import ElementField from './form-element-field.svelte';
import Button from './form-button.svelte';
const Control = FormPrimitive.Control;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import type { InputEvents } from '.';
type $$Props = HTMLInputAttributes;
@@ -14,7 +14,7 @@
<input
class={cn(
'flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-foreground file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
bind:value
on:blur

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
@@ -12,7 +12,7 @@
<LabelPrimitive.Root
class={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className,
className
)}
{...$$restProps}
on:mousedown

View File

@@ -1,7 +1,8 @@
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import Root from './radio-group.svelte';
import Item from './radio-group-item.svelte';
import Root from './radio-group.svelte';
const Input = RadioGroupPrimitive.Input;
export {

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import { Circle } from 'lucide-svelte';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = RadioGroupPrimitive.ItemProps;
type $$Events = RadioGroupPrimitive.ItemEvents;
@@ -15,7 +15,7 @@
{value}
class={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
{...$$restProps}
on:click

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = RadioGroupPrimitive.Props;

View File

@@ -1,10 +1,10 @@
import { Select as SelectPrimitive } from 'bits-ui';
import Label from './select-label.svelte';
import Item from './select-item.svelte';
import Content from './select-content.svelte';
import Trigger from './select-trigger.svelte';
import Item from './select-item.svelte';
import Label from './select-label.svelte';
import Separator from './select-separator.svelte';
import Trigger from './select-trigger.svelte';
const Root = SelectPrimitive.Root;
const Group = SelectPrimitive.Group;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { scale } from 'svelte/transition';
import { cn, flyAndScale } from '$lib/utils';
import { cn, flyAndScale } from '@lib/utils';
type $$Props = SelectPrimitive.ContentProps;
type $$Events = SelectPrimitive.ContentEvents;
@@ -28,7 +28,7 @@
{sideOffset}
class={cn(
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none',
className,
className
)}
{...$$restProps}
on:keydown

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import Check from 'lucide-svelte/icons/check';
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents;
@@ -19,7 +19,7 @@
{label}
class={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
className,
className
)}
{...$$restProps}
on:click

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = SelectPrimitive.LabelProps;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = SelectPrimitive.SeparatorProps;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui';
import ChevronDown from 'lucide-svelte/icons/chevron-down';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = SelectPrimitive.TriggerProps;
type $$Events = SelectPrimitive.TriggerEvents;
@@ -13,7 +13,7 @@
<SelectPrimitive.Trigger
class={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
className
)}
{...$$restProps}
let:builder

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils.js';
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
@@ -14,7 +14,7 @@
bind:checked
class={cn(
'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className,
className
)}
{...$$restProps}
on:click
@@ -22,7 +22,7 @@
>
<SwitchPrimitive.Thumb
class={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitive.Root>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import { cn } from '@lib/utils';
type $$Props = HTMLTextareaAttributes;
@@ -12,7 +12,7 @@
<textarea
class={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
className
)}
bind:value
on:blur

View File

@@ -1,5 +1,6 @@
export const WG_PATH = '/etc/wireguard';
export const WG_SEVER_PATH = `WG::SERVERS`;
export const WG_SEVER_PATH = `WG::SERVER`;
export const WG_AUTH_PATH = `WG::AUTH`;
export const AUTH_COOKIE = 'authorization';

View File

@@ -1,18 +1,26 @@
import 'dotenv/config';
import { randomUUID } from 'node:crypto';
import { createEnv } from '@t3-oss/env-core';
import { z } from 'zod';
import { sha256 } from '$lib/hash';
import { randomUUID } from 'node:crypto';
import 'dotenv/config';
import { sha256 } from '@lib/utils/hash';
export const env = createEnv({
runtimeEnv: process.env,
emptyStringAsUndefined: true,
server: {
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
STORAGE_PATH: z.string().default('/data/storage.pack'),
AUTH_SECRET: z.string().default(sha256(randomUUID())),
HASHED_PASSWORD: z.string().default(sha256('insecure-password')),
// -----
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
ORIGIN: z.string().optional(),
PORT: z.string().optional(),
HOST: z.string().optional(),
// -----
LOG_LEVEL: z.string().default('trace'),
LOG_FILE_PATH: z.string().default('/var/log/wireadmin/web.log'),
LOG_COLORS: z.string().default('true'),
},
});

View File

@@ -1,4 +1,4 @@
export default class ServerError extends Error {
export default class HTTPError extends Error {
statusCode;
constructor(message: string, statusCode: number = 500) {

View File

@@ -1,15 +1,14 @@
import { createWriteStream, promises } from 'node:fs';
import { dirname } from 'node:path';
import { trySafe } from 'p-safe';
import pino, { type Logger, type LoggerOptions } from 'pino';
import pretty from 'pino-pretty';
import { createWriteStream } from 'node:fs';
import { resolve } from 'node:path';
import { fsAccess, fsTouch } from '$lib/fs-extra';
const LOG_LEVEL = process.env.LOG_LEVEL || 'trace';
const LOG_FILE_PATH = process.env.LOG_FILE_PATH || '/var/vlogs/web';
const LOG_COLORS = process.env.LOG_COLORS || 'true';
import { env } from '@lib/env';
import { fsAccess } from '@lib/utils/fs-extra';
const options: LoggerOptions = {
level: LOG_LEVEL,
level: env.LOG_LEVEL,
customLevels: {
trace: 10,
info: 30,
@@ -25,25 +24,41 @@ const jsonLevels = JSON.stringify(options.customLevels);
const levelsInString = jsonLevels.replaceAll('"', '').slice(0, -1).slice(1);
const prettyStream = pretty({
colorize: LOG_COLORS === 'true',
colorize: env.LOG_COLORS === 'true',
customLevels: levelsInString,
});
let logger: Logger = pino(options, pino.multistream([prettyStream]));
if (fsAccess(LOG_FILE_PATH)) {
fsTouch(LOG_FILE_PATH).then(() => {
logger = pino(
options,
pino.multistream([
prettyStream,
createWriteStream(resolve(LOG_FILE_PATH), {
flags: 'a',
}),
]),
);
});
} else {
export function errorBox<T = Error>(e: T) {
console.error('');
console.error('---------------- ERROR ----------------');
logger.error(e);
console.error('---------------- ERROR ----------------');
console.error('');
}
const { error } = await trySafe(async () => {
const logDir = dirname(env.LOG_FILE_PATH);
if (!fsAccess(logDir)) {
await promises.mkdir(logDir, { recursive: true });
}
if (!fsAccess(env.LOG_FILE_PATH)) {
await promises.writeFile(env.LOG_FILE_PATH, '', { encoding: 'utf-8' });
}
logger = pino(
options,
pino.multistream([
prettyStream,
createWriteStream(env.LOG_FILE_PATH, {
flags: 'a',
}),
])
);
});
if (error) {
logger.warn('Log file is not accessible');
}

View File

@@ -1,17 +1,6 @@
import { execa } from 'execa';
import { ip } from 'node-netkit';
export default class Network {
public static async dropInterface(inet: string) {
await execa(`ip link delete dev ${inet}`, { shell: true });
}
public static async defaultInterface(): Promise<string> {
const route = await ip.route.defaultRoute();
if (!route) throw new Error('No default route found');
return route.dev;
}
public static async interfaceExists(inet: string): Promise<boolean> {
try {
const { stdout: o } = await execa(`ip link show | grep ${inet}`, { shell: true });
@@ -25,7 +14,7 @@ export default class Network {
const ports = [];
const { stdout: output } = await execa(
`netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`,
{ shell: true },
{ shell: true }
);
for (const line of output.split('\n')) {
const clean = Number(line.trim());

View File

@@ -1,31 +1,42 @@
import { promises } from 'node:fs';
import { execa } from 'execa';
import logger from '$lib/logger';
import logger from '@lib/logger';
import { fsTouch } from '@lib/utils/fs-extra';
export const SERVICES = <const>{
tor: {
name: 'Tor',
command: {
start: 'screen -dmS "tor" tor -f /etc/tor/torrc',
stop: 'pkill tor',
logs: 'logs tor',
},
start:
'screen -L -Logfile /var/log/wireadmin/tor.log -dmS tor bash -c "screen -S tor -X wrap off; tor -f /etc/tor/torrc"',
stop: 'pkill tor',
logfile: '/var/log/wireadmin/tor.log',
},
};
export type ServiceName = keyof typeof SERVICES;
export function restart(serviceName: ServiceName) {
const service = SERVICES[serviceName];
// Stop
const { exitCode: stopExitCode } = execa(SERVICES[serviceName].command.stop, { shell: true });
const { exitCode: stopCode } = execa(service.stop, { shell: true });
// Start
const { exitCode: startExitCode } = execa(SERVICES[serviceName].command.start, { shell: true });
const { exitCode: startCode } = execa(service.start, { shell: true });
logger.info({
message: `Restarted ${serviceName} service`,
stopExitCode,
startExitCode,
});
logger.info(`Restarted ${serviceName} service. Stats: ${stopCode}:${startCode}`);
return stopExitCode === 0 && startExitCode === 0;
return stopCode === 0 && startCode === 0;
}
export async function logs(serviceName: ServiceName): Promise<string> {
const file = SERVICES[serviceName].logfile;
await fsTouch(file);
return await promises.readFile(file, { encoding: 'utf-8' });
}
export async function clearLogs(serviceName: ServiceName): Promise<void> {
const file = SERVICES[serviceName].logfile;
const cleared = new Date().toISOString();
await promises.writeFile(file, `${cleared} [notice] Log file cleared\n`, { encoding: 'utf-8' });
}

View File

@@ -1,8 +1,40 @@
import { Client, FsDriver, MSGPack } from '@litehex/storage-box';
import { resolve } from 'node:path';
import { env } from '$lib/env';
import { Client, HashMap, MSGPack } from 'storage-box';
import { FsDriver } from 'storage-box/node';
const storagePath = resolve(env.STORAGE_PATH);
const driver = new FsDriver(storagePath, { parser: MSGPack });
import { WG_SEVER_PATH } from '@lib/constants';
import { env } from '@lib/env';
import logger from '@lib/logger';
import type { WgServer } from '@lib/typings';
import { isJson } from '@lib/utils';
export const client = new Client(driver);
const driver = new FsDriver(env.STORAGE_PATH, { parser: MSGPack });
const storage = new Client(driver);
storage._state = 'ready';
export const WG_STORE = storage.createHashMap(
WG_SEVER_PATH,
class extends HashMap<string, WgServer> {
async listServers(): Promise<WgServer[]> {
const raw = await this.getall();
const list = [];
for (const [key, value] of Object.entries(raw)) {
if (typeof value === 'string' && isJson(value)) {
list.push(JSON.parse(value));
} else if (typeof value === 'object') {
list.push(value);
} else {
logger.warn(`WireGuard: ListServers: malformed server: ${key}`);
}
}
return list;
}
async findByHash(hash: string): Promise<WgServer | undefined> {
const servers = await this.listServers();
return servers.find((s) => s.confHash === hash);
}
}
);
export { storage };

View File

@@ -1,6 +1,7 @@
import { z } from 'zod';
import { NameSchema, TorSchema } from '$lib/wireguard/schema';
import { IPV4_REGEX } from 'node-netkit/ip';
import { z } from 'zod';
import { NameSchema, TorSchema } from '@lib/wireguard/schema';
export const WgKeySchema = z.object({
privateKey: z.string(),

View File

@@ -1,7 +1,7 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { clsx, type ClassValue } from 'clsx';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
import { twMerge } from 'tailwind-merge';
export function isJson(data: any): boolean {
if (typeof data !== 'string') {
@@ -36,7 +36,7 @@ type FlyAndScaleParams = {
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 },
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;

View File

@@ -1,18 +1,19 @@
import fs from 'node:fs';
import fs, { promises } from 'node:fs';
import path from 'path';
import deepmerge from 'deepmerge';
import type { Peer, WgKey, WgServer } from '$lib/typings';
import Network from '$lib/network';
import { WG_PATH, WG_SEVER_PATH } from '$lib/constants';
import { dynaJoin, isJson, sleep } from '$lib/utils';
import { getPeerConf } from '$lib/wireguard/utils';
import logger from '$lib/logger';
import { sha256 } from '$lib/hash';
import { fsAccess } from '$lib/fs-extra';
import { client } from '$lib/storage';
import { execa } from 'execa';
import { ip } from 'node-netkit';
import { WG_PATH } from '@lib/constants';
import logger from '@lib/logger';
import Network from '@lib/network';
import { WG_STORE } from '@lib/storage';
import type { Peer, WgKey, WgServer } from '@lib/typings';
import { dynaJoin, sleep } from '@lib/utils';
import { fsAccess } from '@lib/utils/fs-extra';
import { sha256 } from '@lib/utils/hash';
import { getPeerConf } from '@lib/wireguard/utils';
export class WGServer {
readonly id: string;
readonly peers: WGPeers;
@@ -30,34 +31,31 @@ export class WGServer {
this.peers = new WGPeers(this);
}
static exists(id: string): boolean {
const serverIds = getServers().map((s) => s.id);
const exists = serverIds.includes(id);
static async exists(id: string): Promise<boolean> {
const exists = await WG_STORE.exists(id);
if (!exists) {
logger.debug({
message: `WGServer: Exists: server by id of ${id} does not exists`,
servers: serverIds,
});
logger.debug(`WireGuard: ServerExists: server does not exists. Id: ${id}`);
logger.debug(await WG_STORE.getall());
return false;
}
return exists;
return true;
}
async get(): Promise<WgServer> {
if (!fsAccess(WG_PATH)) {
logger.debug('WGServer: get: creating wg path');
fs.mkdirSync(WG_PATH, { recursive: true, mode: 0o600 });
logger.debug(`WireGuard: Get: path does not exists. Path: ${WG_PATH}`);
await promises.mkdir(WG_PATH, { recursive: true, mode: 0o600 });
}
const server = await findServer(this.id);
const server = await WG_STORE.get(this.id);
if (!server) {
throw new Error('WGServer: get: server not found');
throw new Error('WireGuard: Server not found');
}
if (!fsAccess(resolveConfigPath(server.confId))) {
logger.debug('WGServer: get: creating config file');
const confPath = resolveConfigPath(server.confId);
if (!fsAccess(confPath)) {
logger.debug(`WireGuard: Get: config file does not exists. Path: ${confPath}`);
await this.writeConfigFile(server);
}
@@ -80,7 +78,7 @@ export class WGServer {
return true;
}
async start(): Promise<boolean> {
async start(): Promise<void> {
const server = await this.get();
const HASH = getConfigHash(server.confId);
@@ -97,40 +95,31 @@ export class WGServer {
await execa(`wg-quick up wg${server.confId}`, { shell: true });
await this.update({ status: 'up' });
return true;
}
async remove(): Promise<boolean> {
const server = await this.get();
await this.stop();
async remove(): Promise<void> {
const server = await WG_STORE.get(this.id);
if (!server) {
logger.warn(`WireGuard: Remove: server not found. Id: ${this.id}`);
return;
}
if (wgConfExists(server.confId)) {
logger.debug('WGServer:Remove: removing config file');
fs.unlinkSync(resolveConfigPath(server.confId));
logger.debug(`WireGuard: Remove: deleting config file. Id: ${this.id}`);
await promises.unlink(resolveConfigPath(server.confId));
}
const index = await findServerIndex(this.id);
if (typeof index !== 'number') {
logger.warn('WGServer:Remove: server index not found');
return true;
}
client.ldel(WG_SEVER_PATH, index);
return true;
await WG_STORE.del(this.id);
}
async update(update: Partial<WgServer>): Promise<boolean> {
const server = await this.get();
const index = await findServerIndex(this.id);
if (typeof index !== 'number') {
logger.warn('WGServer:Update: server index not found');
const server = await WG_STORE.get(this.id);
if (!server) {
logger.warn(`WireGuard: Update: server not found. Id: ${this.id}`);
return true;
}
client.lset(WG_SEVER_PATH, index, {
await WG_STORE.set(this.id, {
...deepmerge(server, update),
peers: update?.peers || server?.peers || [],
updatedAt: new Date().toISOString(),
@@ -190,7 +179,7 @@ export class WGServer {
}
static async getFreePeerIp(serverId: string): Promise<string | undefined> {
const server = await findServer(serverId);
const server = await WG_STORE.get(serverId);
if (!server) {
logger.error('WGServer: GetFreePeerIP: no server found');
return undefined;
@@ -243,18 +232,12 @@ class WGPeers {
peer.preSharedKey && `PresharedKey = ${peer.preSharedKey}`,
`AllowedIPs = ${peer.allowedIps}/32`,
peer.persistentKeepalive && `PersistentKeepalive = ${peer.persistentKeepalive}`,
]),
])
);
fs.writeFileSync(confPath, lines.join('\n'), { mode: 0o600 });
await this.server.update({ confHash: getConfigHash(server.confId) });
const index = await findServerIndex(this.server.id);
if (typeof index !== 'number') {
logger.warn('WGPeers:Add: server index not found');
return true;
}
client.lset(WG_SEVER_PATH, index, {
await WG_STORE.set(this.server.id, {
...server,
peers: [...server.peers, peer],
});
@@ -271,13 +254,7 @@ class WGPeers {
const server = await this.server.get();
const peers = wgPeersStr(server.confId);
const index = await findServerIndex(this.server.id);
if (typeof index !== 'number') {
logger.warn('WGPeers:Remove: server index not found');
return true;
}
client.lset(WG_SEVER_PATH, index, {
await WG_STORE.set(this.server.id, {
...server,
peers: server.peers.filter((p) => p.publicKey !== publicKey),
});
@@ -306,18 +283,12 @@ class WGPeers {
async update(publicKey: string, update: Partial<Peer>): Promise<boolean> {
const server = await this.server.get();
const index = await findServerIndex(this.server.id);
if (typeof index !== 'number') {
logger.warn('WGPeers:Update: server index not found');
return true;
}
const updatedPeers = server.peers.map((p) => {
if (p.publicKey !== publicKey) return p;
return deepmerge(p, update);
});
client.lset(WG_SEVER_PATH, index, { ...server, peers: updatedPeers });
await WG_STORE.set(this.server.id, { ...server, peers: updatedPeers });
await this.storePeers(publicKey, updatedPeers);
if (server.status === 'up') {
@@ -334,7 +305,7 @@ class WGPeers {
}
async generateConfig(peerId: string): Promise<string | undefined> {
const server = await findServer(this.server.id);
const server = await WG_STORE.get(this.server.id);
if (!server) {
logger.error('WGPeers:GeneratePeerConfig: server not found');
return undefined;
@@ -476,28 +447,6 @@ function wgConfExists(configId: number): boolean {
return fsAccess(confPath);
}
/**
* Used to read /etc/wireguard/*.conf and sync them with our
* redis server.
*/
async function syncServers(): Promise<boolean> {
// get files in /etc/wireguard
const files = fs.readdirSync(WG_PATH);
// filter files that start with wg and end with .conf
const reg = new RegExp(/^wg(\d+)\.conf$/);
const confs = files.filter((f) => reg.test(f));
// read all confs
const servers = await Promise.all(confs.map((f) => readWgConf(parseInt(f.match(reg)![1]))));
// remove old servers
client.del(WG_SEVER_PATH);
// save all servers to redis
client.lpush(WG_SEVER_PATH, servers);
return true;
}
function wgPeersStr(configId: number): string[] {
const confPath = path.resolve(WG_PATH, `wg${configId}.conf`);
const conf = fs.readFileSync(confPath, 'utf-8');
@@ -531,7 +480,7 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise<
const uuid = crypto.randomUUID();
logger.debug(
`WireGuard: GenerateWgServer: creating server with id: ${uuid} and confId: ${confId}`,
`WireGuard: GenerateWgServer: creating server with id: ${uuid} and confId: ${confId}`
);
let server: WgServer = {
@@ -572,7 +521,7 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise<
// save server config
logger.debug('WireGuard: GenerateWgServer: saving server to storage');
logger.debug(server);
client.lpush(WG_SEVER_PATH, server);
await WG_STORE.set(uuid, server);
const CONFIG_PATH = resolveConfigPath(confId);
@@ -598,17 +547,20 @@ export async function generateWgServer(config: GenerateWgServerParams): Promise<
}
export async function isIPReserved(ip: string): Promise<boolean> {
const addresses = getServers().map((s) => s.address);
const severs = await WG_STORE.listServers();
const addresses = severs.map((s) => s.address);
return addresses.includes(ip);
}
export async function isPortReserved(port: number): Promise<boolean> {
const inUsePorts = [await Network.inUsePorts(), getServers().map((s) => Number(s.listen))].flat();
const severs = await WG_STORE.listServers();
const inUsePorts = [await Network.inUsePorts(), severs.map((s) => Number(s.listen))].flat();
return inUsePorts.includes(port);
}
export async function isConfigIdReserved(id: number): Promise<boolean> {
const ids = getServers().map((s) => s.confId);
const severs = await WG_STORE.listServers();
const ids = severs.map((s) => s.confId);
return ids.includes(id);
}
@@ -651,52 +603,12 @@ export function maxConfId(): number {
return Math.max(0, ...ids);
}
export function getServers(): WgServer[] {
const rawServers = (client.list(WG_SEVER_PATH) || []) as string[];
return rawServers.map((s) => {
if (isJson(s)) {
return JSON.parse(s);
}
if (typeof s === 'object') {
return s;
}
logger.warn('WireGuard: GetServers: invalid server found');
return null;
});
}
export async function findServerIndex(id: string): Promise<number | undefined> {
let index = 0;
const servers = getServers();
for (const s of servers) {
if (s.id === id) {
return index;
}
index++;
}
return undefined;
}
export async function findServer(
id: string | undefined,
hash?: string,
): Promise<WgServer | undefined> {
const servers = getServers();
return id
? servers.find((s) => s.id === id)
: hash && isJson(hash)
? servers.find((s) => JSON.stringify(s) === hash)
: undefined;
}
export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: string }> {
const source = `${s.address}/24`;
const route = await ip.route.defaultRoute();
const wg_inet = `wg${s.confId}`;
const loopback = '127.0.0.1';
if (!route) {
throw new Error('No default route found');
@@ -705,18 +617,44 @@ export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: s
const { stdout: inet_address } = await execa(`hostname -i | awk '{print $1}'`, { shell: true });
if (s.tor) {
const up = dynaJoin([
`iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT`,
`iptables -A INPUT -i ${wg_inet} -s ${source} -m state --state NEW -j ACCEPT`,
`iptables -t nat -A PREROUTING -i ${wg_inet} -p udp -s ${source} --dport 53 -j DNAT --to-destination ${inet_address}:53530`,
`iptables -t nat -A PREROUTING -i ${wg_inet} -p tcp -s ${source} -j DNAT --to-destination ${inet_address}:59040`,
`iptables -t nat -A PREROUTING -i ${wg_inet} -p udp -s ${source} -j DNAT --to-destination ${inet_address}:59040`,
`iptables -t nat -A OUTPUT -o lo -j RETURN`,
`iptables -A OUTPUT -m conntrack --ctstate INVALID -j DROP`,
`iptables -A OUTPUT -m state --state INVALID -j DROP`,
`iptables -A OUTPUT ! -o lo ! -d 127.0.0.1 ! -s 127.0.0.1 -p tcp -m tcp --tcp-flags ACK,FIN ACK,FIN -j DROP`,
]).join('; ');
return { up, down: up.replace(/-A/g, '-D') };
// https://trac.torproject.org/projects/tor/wiki/doc/TransparentProxy#WARNING
// https://lists.torproject.org/pipermail/tor-talk/2014-March/032503.html
const out_iface = route.dev;
const virt_addr = '10.192.0.0/10';
const trans_port = '59040';
const dns_port = '53530';
const non_tor = '127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16'.split(' ');
const resv_iana =
'0.0.0.0/8 100.64.0.0/10 169.254.0.0/16 192.0.0.0/24 192.0.2.0/24 192.88.99.0/24 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4 255.255.255.255/32'.split(
' '
);
const up = dynaJoin(
[
`iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT`,
`iptables -t nat -A INPUT -i lo -j ACCEPT`,
`iptables -A OUTPUT -d 127.0.0.1/32 -o lo -j ACCEPT`,
`iptables -A INPUT -i ${wg_inet} -s ${source} -m state --state NEW -j ACCEPT`,
// nat dns requests to Tor
`iptables -t nat -A PREROUTING -i ${wg_inet} -p udp --dport 53 -j DNAT --to-destination ${inet_address}:${dns_port}`,
// Redirect all other pre-routing and output to Tor's TransPort
`iptables -t nat -A PREROUTING -i ${wg_inet} -p tcp -j DNAT --to-destination ${inet_address}:${trans_port}`,
`iptables -t nat -A PREROUTING -i ${wg_inet} -p udp -j DNAT --to-destination ${inet_address}:${trans_port}`,
// Allow Tor process output
`iptables -A OUTPUT -o ${out_iface} -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m state --state NEW -j ACCEPT`,
// nat .onion addresses
`iptables -t nat -A OUTPUT -d ${virt_addr} -p tcp -m tcp -j DNAT --to-destination ${inet_address}:${trans_port}`,
`iptables -A OUTPUT -m state --state ESTABLISHED -j ACCEPT`,
`iptables -A OUTPUT -m conntrack --ctstate INVALID -j DROP`,
`iptables -A OUTPUT -m state --state INVALID -j DROP`,
// Allow lan access for hosts in $non_tor
[non_tor, resv_iana].flat().map((n) => `iptables -t nat -A OUTPUT -d ${n} -j RETURN`),
// Don't nat the Tor process, the loopback, or the local network
`iptables -t nat -A OUTPUT -o lo -j RETURN`,
`iptables -A OUTPUT ! -o lo ! -d ${loopback} ! -s ${loopback} -p tcp -m tcp --tcp-flags ACK,FIN ACK,FIN -j DROP`,
`iptables -A OUTPUT ! -o lo ! -d ${loopback} ! -s ${loopback} -p tcp -m tcp --tcp-flags ACK,RST ACK,RST -j DROP`,
].flat()
).join('; ');
return { up, down: up.replace(/ -A /g, ' -D ') };
}
const up = dynaJoin([
@@ -755,7 +693,7 @@ export async function genServerConf(server: WgServer): Promise<string> {
lines.push(`${peer.preSharedKey ? `PresharedKey = ${peer.preSharedKey}` : 'OMIT'}`);
lines.push(`AllowedIPs = ${peer.allowedIps}/32`);
lines.push(
`${peer.persistentKeepalive ? `PersistentKeepalive = ${peer.persistentKeepalive}` : 'OMIT'}`,
`${peer.persistentKeepalive ? `PersistentKeepalive = ${peer.persistentKeepalive}` : 'OMIT'}`
);
});

View File

@@ -1,6 +1,7 @@
import { z } from 'zod';
import { isBetween } from '$lib/utils/number';
import { IPV4_REGEX, isPrivateIP } from '$lib/utils/ip';
import { IPV4_REGEX, isPrivateIP } from '@lib/utils/ip';
import { isBetween } from '@lib/utils/number';
export const NameSchema = z
.string()
@@ -29,7 +30,7 @@ export const PortSchema = z
},
{
message: 'Port must be a valid port number',
},
}
);
export const TorSchema = z.boolean().default(false);

View File

@@ -1,4 +1,4 @@
import type { WgServer } from '$lib/typings';
import type { WgServer } from '@lib/typings';
type Peer = WgServer['peers'][0];

View File

@@ -1,15 +1,5 @@
import type { ZodError } from 'zod';
// export function zodErrorToResponse(res: NextApiResponse, z: ZodError) {
// return res
// .status(400)
// .json({
// ok: false,
// message: 'Bad Request',
// details: zodErrorMessage(z)
// })
// }
export function zodEnumError(message: string) {
return { message };
}

View File

@@ -4,6 +4,6 @@
import { ModeWatcher } from 'mode-watcher';
</script>
<ModeWatcher />
<ModeWatcher defaultMode="system" />
<slot />
<Toaster />

View File

@@ -1,22 +1,18 @@
import { type Actions, error, fail } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import {
findServer,
generateWgServer,
getServers,
isIPReserved,
isPortReserved,
WGServer,
} from '$lib/wireguard';
import { error, fail, type Actions } from '@sveltejs/kit';
import { setError, superValidate } from 'sveltekit-superforms';
import { createServerSchema } from './schema';
import { NameSchema } from '$lib/wireguard/schema';
import logger from '$lib/logger';
import { zod } from 'sveltekit-superforms/adapters';
import logger, { errorBox } from '@lib/logger';
import { WG_STORE } from '@lib/storage';
import { generateWgServer, isIPReserved, isPortReserved, WGServer } from '@lib/wireguard';
import { NameSchema } from '@lib/wireguard/schema';
import type { PageServerLoad } from './$types';
import { createServerSchema } from './schema';
export const load: PageServerLoad = async () => {
return {
servers: getServers(),
servers: await WG_STORE.listServers(),
form: await superValidate(zod(createServerSchema)),
};
};
@@ -27,15 +23,15 @@ export const actions: Actions = {
const serverId = (form.get('id') ?? '').toString();
const name = (form.get('name') ?? '').toString();
const server = await findServer(serverId ?? '');
const server = await WG_STORE.get(serverId);
if (!server) {
logger.error('Actions: RenameServer: Server not found');
return error(404, 'Not found');
logger.error(`WebUI: Actions: RenameServer: Server not found. Id: ${serverId}`);
throw error(404, 'Not found');
}
if (!NameSchema.safeParse(name).success) {
logger.error('Actions: RenameServer: Server name is invalid');
return error(400, 'Bad Request');
throw error(400, 'Bad Request');
}
const wg = new WGServer(server.id);
@@ -76,7 +72,7 @@ export const actions: Actions = {
serverId,
};
} catch (e) {
logger.error(e);
errorBox(e);
return setError(form, 'Unhandled Exception');
}
},

View File

@@ -1,13 +1,15 @@
<script lang="ts">
import BasePage from '$lib/components/page/BasePage.svelte';
import BasePage from '@lib/components/page/BasePage.svelte';
import type { PageData } from './$types';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@lib/components/ui/card';
import Service from './Service.svelte';
import { Empty } from '$lib/components/empty';
import { Empty } from '@lib/components/empty';
import Server from './Server.svelte';
import fetchAction from '$lib/fetch-action';
import fetchAction from '@lib/utils/fetch-action';
import CreateServerDialog from './CreateServerDialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Button } from '@lib/components/ui/button';
import { PlusIcon } from 'lucide-svelte';
import { OnionIcon } from '@lib/components/iconset';
export let data: PageData;
@@ -35,8 +37,8 @@
<h2 class={'font-bold text-xl'}>Hello there 👋</h2>
<CreateServerDialog data={data.form} let:builder>
<Button builders={[builder]}>
<i class="fas fa-plus mr-2"></i>
<Button builders={[builder]} size="sm">
<PlusIcon class="mr-2" />
Create Server
</Button>
</CreateServerDialog>
@@ -67,10 +69,10 @@
<CardContent>
<Service name="Tor" slug="tor">
<svelte:fragment slot="icon">
<i class={'fa-solid fa-onion text-purple-700 text-xl'} />
<OnionIcon class="text-purple-700 text-xl" />
</svelte:fragment>
</Service>
</CardContent>
</Card>
</div></BasePage
>
</div>
</BasePage>

View File

@@ -8,7 +8,7 @@
DialogHeader,
DialogTitle,
DialogTrigger,
} from '$lib/components/ui/dialog';
} from '@lib/components/ui/dialog';
import {
FormButton,
FormControl,
@@ -16,20 +16,20 @@
FormField,
FormFieldErrors,
FormLabel,
} from '$lib/components/ui/form';
import { cn } from '$lib/utils';
} from '@lib/components/ui/form';
import { cn } from '@lib/utils';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '$lib/components/ui/collapsible';
import { Button } from '$lib/components/ui/button';
} from '@lib/components/ui/collapsible';
import { Button } from '@lib/components/ui/button';
import toast from 'svelte-french-toast';
import { Input } from '$lib/components/ui/input';
import { Input } from '@lib/components/ui/input';
import { zodClient } from 'sveltekit-superforms/adapters';
import { Switch } from '$lib/components/ui/switch';
import { Switch } from '@lib/components/ui/switch';
import { ChevronRightIcon, LoaderCircle } from 'lucide-svelte';
let loading: boolean = false;
let dialogOpen = false;
export let data: SuperValidated<Infer<CreateServerSchemaType>>;
@@ -37,15 +37,10 @@
const form = superForm(data, {
dataType: 'json',
validators: zodClient(createServerSchema),
onSubmit: () => {
loading = true;
},
onError: (e) => {
loading = false;
console.error('Client-side: FormError:', e);
},
onResult: ({ result }) => {
loading = false;
if (result.type === 'success') {
dialogOpen = false;
toast.success('Server created successfully!');
@@ -56,7 +51,7 @@
},
});
const { form: formData, enhance } = form;
const { form: formData, enhance, submitting } = form;
</script>
<Dialog bind:open={dialogOpen}>
@@ -101,16 +96,20 @@
<FormLabel>Port</FormLabel>
<Input {...attrs} bind:value={$formData.port} placeholder={'e.g. 51820'} type={'text'} />
</FormControl>
<FormDescription>This is the port that the WireGuard server will listen on.</FormDescription
>
<FormDescription>
This is the port that the WireGuard server will listen on.
</FormDescription>
<FormFieldErrors />
</FormField>
<Collapsible>
<CollapsibleTrigger asChild let:builder>
<Button builders={[builder]} variant="ghost" size="sm" class="mb-4 -mr-2">
<i
class={cn('far fa-cog mr-2', builder['data-state'] === 'open' ? 'animate-spin' : '')}
<ChevronRightIcon
class={cn(
'mr-2 h-4 w-4 duration-200 ease-in-out transform',
builder['data-state'] === 'open' ? 'rotate-90' : 'rotate-0'
)}
/>
<span>Advanced Options</span>
</Button>
@@ -118,21 +117,28 @@
<CollapsibleContent class="space-y-6">
<FormField {form} name={'tor'}>
<FormControl let:attrs>
<Switch {...attrs} bind:checked={$formData.tor} />
<FormLabel>Use Tor</FormLabel>
</FormControl>
<div class="flex items-center space-x-2">
<FormControl let:attrs>
<Switch {...attrs} bind:checked={$formData.tor} />
<FormLabel>Use Tor</FormLabel>
</FormControl>
</div>
<FormDescription>This will route all outgoing traffic through Tor.</FormDescription>
</FormField>
<FormField {form} name={'dns'}>
<FormControl let:attrs>
<FormLabel>DNS</FormLabel>
<Input placeholder={'e.g. 1.1.1.1'} type={'text'} />
<Input
{...attrs}
bind:value={$formData.dns}
placeholder={'e.g. 9.9.9.9, 9.9.9.10'}
type={'text'}
/>
</FormControl>
<FormDescription
>Optional. This is the DNS server that will be pushed to clients.</FormDescription
>
<FormDescription>
Optional. This is the DNS server that will be pushed to clients.
</FormDescription>
<FormFieldErrors />
</FormField>
@@ -148,9 +154,8 @@
</Collapsible>
<DialogFooter>
<FormButton>
<i class={cn(loading ? 'far fa-arrow-rotate-right animate-spin' : 'far fa-plus', 'mr-2')}
></i>
<FormButton disabled={$submitting}>
<LoaderCircle class={cn('mr-2 h-4 w-4 animate-spin', !$submitting && 'hidden')} />
Create
</FormButton>
</DialogFooter>

Some files were not shown because too many files have changed in this diff Show More