mirror of
https://github.com/wireadmin/wireadmin
synced 2025-06-26 18:28:06 +00:00
update
This commit is contained in:
@@ -74,6 +74,9 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
web/src/lib/components/iconset/icon.svelte
Normal file
20
web/src/lib/components/iconset/icon.svelte
Normal 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>
|
||||
21
web/src/lib/components/iconset/index.ts
Normal file
21
web/src/lib/components/iconset/index.ts
Normal 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,
|
||||
};
|
||||
15
web/src/lib/components/iconset/onion-icon.svelte
Normal file
15
web/src/lib/components/iconset/onion-icon.svelte
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export { default as Badge } from './badge.svelte';
|
||||
|
||||
export const badgeVariants = tv({
|
||||
|
||||
@@ -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>
|
||||
16
web/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
Normal file
16
web/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte
Normal 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>
|
||||
31
web/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
Normal file
31
web/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte
Normal 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}
|
||||
23
web/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
Normal file
23
web/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte
Normal 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>
|
||||
23
web/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
Normal file
23
web/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte
Normal 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>
|
||||
@@ -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>
|
||||
15
web/src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal file
15
web/src/lib/components/ui/breadcrumb/breadcrumb.svelte
Normal 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>
|
||||
25
web/src/lib/components/ui/breadcrumb/index.ts
Normal file
25
web/src/lib/components/ui/breadcrumb/index.ts
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Root from './checkbox.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
|
||||
|
||||
import Content from './collapsible-content.svelte';
|
||||
|
||||
const Root = CollapsiblePrimitive.Root;
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
@@ -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
|
||||
/>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default class ServerError extends Error {
|
||||
export default class HTTPError extends Error {
|
||||
statusCode;
|
||||
|
||||
constructor(message: string, statusCode: number = 500) {
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'}`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WgServer } from '$lib/typings';
|
||||
import type { WgServer } from '@lib/typings';
|
||||
|
||||
type Peer = WgServer['peers'][0];
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<ModeWatcher defaultMode="system" />
|
||||
<slot />
|
||||
<Toaster />
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user