deleted old dashboard
This commit is contained in:
parent
2ac2650f99
commit
5e3fb972e8
@ -1,74 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import { slide } from "svelte/transition";
|
|
||||||
import IconListDetails from "~icons/tabler/list-details";
|
|
||||||
import IconMinus from "~icons/tabler/minus";
|
|
||||||
import IconPlus from "~icons/tabler/plus";
|
|
||||||
import IconSearch from "~icons/tabler/search";
|
|
||||||
import IconX from "~icons/tabler/x";
|
|
||||||
import Button from "../ui/Button.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
label,
|
|
||||||
sublabel = "",
|
|
||||||
name,
|
|
||||||
data
|
|
||||||
}: {
|
|
||||||
label: string,
|
|
||||||
sublabel?: string,
|
|
||||||
name: string,
|
|
||||||
data: {
|
|
||||||
value: string,
|
|
||||||
name: string,
|
|
||||||
selected: boolean
|
|
||||||
}[]
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
let selectData = $state(data);
|
|
||||||
let expanded = $state(false);
|
|
||||||
let search = $state("");
|
|
||||||
let focused = $state(false);
|
|
||||||
|
|
||||||
if (browser) {
|
|
||||||
document.addEventListener("click", () => {
|
|
||||||
if (!focused) expanded = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex gap-x-10 gap-y-3 flex-wrap mb-5">
|
|
||||||
<label for={name} class="block w-1/3 min-w-[300px]">
|
|
||||||
<p>{label}</p>
|
|
||||||
{#if sublabel}
|
|
||||||
<p class="text-sm text-neutral-400 dark:text-neutral-500">{sublabel}</p>
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
<select multiple name={name} class="hidden">
|
|
||||||
{#each selectData.filter(d => d.selected) as el}
|
|
||||||
<option value={el.value} selected></option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div class="relative flex gap-10 items-center h-fit rounded border border-gray-300 dark:border-neutral-700 shadow-sm dark:shadow-neutral-800 pl-4 pr-2 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
|
||||||
{#if selectData.length == 0}
|
|
||||||
<p>None available</p>
|
|
||||||
{:else}
|
|
||||||
<p>{selectData.filter(d => d.selected).length} selected</p>
|
|
||||||
|
|
||||||
<Button Icon={expanded ? IconX : IconListDetails} type="button" extra="!px-2 !py-1 !text-xs !rounded-sm" onclick={() => {expanded = !expanded; focused = true}} onmouseleave={() => focused = false}>{expanded ? "Close" : "Select"}</Button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if expanded}
|
|
||||||
<ul transition:slide onmouseleave={() => focused = false} onmouseenter={() => focused = true} class="absolute flex flex-col gap-2 top-[calc(100%+10px)] z-1 left-0 w-full max-h-40 overflow-y-scroll bg-white dark:bg-neutral-950 rounded border border-gray-300 dark:border-neutral-700 shadow-sm p-2 text-xs">
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<IconSearch class="text-neutral-400"/>
|
|
||||||
<input type="text" placeholder="Search..." bind:value={search} class="block w-full text-sm !outline-none transition-all duration-300 ease-in-out border-transparent focus:border-b-2 focus:border-indigo-500">
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-px bg-neutral-200 dark:bg-neutral-700"></div>
|
|
||||||
{#each search ? selectData.filter(d => d.name.toLowerCase().includes(search.toLowerCase())) : selectData as el}
|
|
||||||
<Button Icon={el.selected ? IconMinus : IconPlus} type="button" extra="!p-1 !text-xs !rounded-sm w-full !border-none" onclick={() => {el.selected = !el.selected}} inverted={!el.selected}>{el.name}</Button>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let {
|
|
||||||
label,
|
|
||||||
sublabel = "",
|
|
||||||
name,
|
|
||||||
defaultValue,
|
|
||||||
...others
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex gap-x-10 gap-y-3 flex-wrap mb-5 items-center">
|
|
||||||
<label for={name} class="block w-1/3 min-w-[300px]">
|
|
||||||
<p>{label}</p>
|
|
||||||
{#if sublabel}
|
|
||||||
<p class="text-sm text-neutral-400 dark:text-neutral-500">{sublabel}</p>
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
<input id={name} name={name} type="text" defaultValue={defaultValue} {...others}
|
|
||||||
class="block h-fit rounded border border-gray-300 dark:border-neutral-700 shadow-sm dark:shadow-neutral-800 px-4 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
|
||||||
</div>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import IconCheck from "~icons/tabler/check";
|
|
||||||
import IconX from "~icons/tabler/x";
|
|
||||||
import Button from "../ui/Button.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
label,
|
|
||||||
sublabel = "",
|
|
||||||
name,
|
|
||||||
checked,
|
|
||||||
options = ["Yes", "No"],
|
|
||||||
...others
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex gap-x-10 gap-y-3 flex-wrap mb-5 items-center">
|
|
||||||
<label for={name} class="block w-1/3 min-w-[300px]">
|
|
||||||
<p>{label}</p>
|
|
||||||
{#if sublabel.length > 0}
|
|
||||||
<p class="text-sm text-neutral-400 dark:text-neutral-500">{sublabel}</p>
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input class="hidden" type="checkbox" id={name} name={name} defaultChecked={checked}/>
|
|
||||||
<Button Icon={checked ? IconCheck : IconX} type="button" inverted={!checked} {...others} onclick={() => checked = !checked}>{checked ? options[0] : options[1]}</Button>
|
|
||||||
</div>
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let {
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
actionsSnippet
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex gap-10 justify-between items-center rounded border border-neutral-200 dark:border-neutral-700 py-4 px-6 shadow dark:shadow-neutral-800">
|
|
||||||
<div>
|
|
||||||
<p class="font-medium">{title}</p>
|
|
||||||
<p class="text-neutral-400">{subtitle}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
{#if actionsSnippet}
|
|
||||||
{@render actionsSnippet()}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import IconChevronLeft from "~icons/tabler/chevron-left";
|
|
||||||
import IconEclamationCircle from "~icons/tabler/exclamation-circle";
|
|
||||||
import Button from "../ui/Button.svelte";
|
|
||||||
import HorizontalSpacer from "../ui/HorizontalSpacer.svelte";
|
|
||||||
import ResourcePageHeader from "./ResourcePageHeader.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
heading,
|
|
||||||
children,
|
|
||||||
error = null,
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (error) {
|
|
||||||
setTimeout(() => error = null, 5000)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-col h-full text-neutral-700 dark:text-neutral-300">
|
|
||||||
<ResourcePageHeader title={heading}>
|
|
||||||
<Button Icon={IconChevronLeft} onclick={() => history.back()} inverted>Back</Button>
|
|
||||||
</ResourcePageHeader>
|
|
||||||
|
|
||||||
<HorizontalSpacer/>
|
|
||||||
|
|
||||||
<div class="text-sm flex-1 overflow-scroll">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<div transition:fade>
|
|
||||||
<Button Icon={IconEclamationCircle} type="button" color="danger" onclick={() => error = null}>{error} (Click to dismiss)</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import HorizontalSpacer from "../ui/HorizontalSpacer.svelte";
|
|
||||||
import ResourcePageHeader from "./ResourcePageHeader.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
heading,
|
|
||||||
actionSnippet,
|
|
||||||
contentSnippet
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-col h-full text-neutral-700 dark:text-neutral-300">
|
|
||||||
<ResourcePageHeader title={heading}>
|
|
||||||
{@render actionSnippet()}
|
|
||||||
</ResourcePageHeader>
|
|
||||||
|
|
||||||
<HorizontalSpacer/>
|
|
||||||
|
|
||||||
<div class="text-sm flex-1 overflow-scroll">
|
|
||||||
{@render contentSnippet()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let { title, children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<h1 class="text-xl font-semibold">{title}</h1>
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { ClassValue, HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
|
||||||
|
|
||||||
let {
|
|
||||||
a,
|
|
||||||
color,
|
|
||||||
inverted,
|
|
||||||
Icon,
|
|
||||||
children,
|
|
||||||
extra,
|
|
||||||
...others
|
|
||||||
}: {
|
|
||||||
a?: boolean,
|
|
||||||
color?: "primary" | "danger" | "success",
|
|
||||||
inverted?: boolean,
|
|
||||||
Icon?: any,
|
|
||||||
extra?: ClassValue,
|
|
||||||
children?: any
|
|
||||||
} & HTMLButtonAttributes & HTMLAnchorAttributes = $props();
|
|
||||||
|
|
||||||
const baseClasses = "block flex items-center text-sm px-4 py-2 cursor-pointer rounded transition-all duration-300 ease-in-out";
|
|
||||||
|
|
||||||
const colors = {
|
|
||||||
"normal": {
|
|
||||||
"danger": "bg-red-500 hover:bg-red-600 text-white border border-transparent",
|
|
||||||
"success": "bg-emerald-500 hover:bg-emerald-600 text-white border border-transparent",
|
|
||||||
"primary": "bg-neutral-800 dark:bg-neutral-200 hover:bg-neutral-600 dark:hover:bg-neutral-400 text-white dark:text-black border border-transparent",
|
|
||||||
},
|
|
||||||
"inverted": {
|
|
||||||
"danger": "bg-white dark:bg-black hover:bg-red-100 dark:hover:bg-red-950 text-red-500 border border-red-500",
|
|
||||||
"success": "bg-white hover:bg-emerald-100 text-emerald-500 border border-emerald-500",
|
|
||||||
"primary": "bg-white dark:bg-neutral-950 hover:bg-neutral-100 dark:hover:bg-neutral-800 text-neutral-800 dark:text-neutral-200 border border-neutral-800 dark:border-neutral-700",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let colorClasses = $derived(colors[inverted ? "inverted" : "normal"][color ?? "primary"]);
|
|
||||||
|
|
||||||
let fullClasses = $derived(baseClasses + " " + extra + " " + colorClasses);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
{#if a}
|
|
||||||
<a class={fullClasses} {...others}>
|
|
||||||
{#if Icon}
|
|
||||||
<Icon class={children ? "mr-2" : ""}></Icon>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{@render children?.()}
|
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<button class={fullClasses} {...others}>
|
|
||||||
{#if Icon}
|
|
||||||
<Icon class={children ? "mr-2" : ""}></Icon>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{@render children?.()}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import IconChevronRight from "~icons/tabler/chevron-right";
|
|
||||||
import { slide } from "svelte/transition";
|
|
||||||
let { children, label } = $props();
|
|
||||||
let expanded = $state(false);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<button type="button" class="flex items-center gap-2 cursor-pointer" onclick={() => expanded = !expanded}>
|
|
||||||
<IconChevronRight class="{expanded ? "rotate-90" : ""} transition-all duration-300"/>
|
|
||||||
<h1 class="text-xs uppercase font-light">{label}</h1>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if expanded}
|
|
||||||
<div transition:slide={{duration: 300}} class="mt-4">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<div class="w-full h-px bg-neutral-200 dark:bg-neutral-700 my-4"></div>
|
|
||||||
@ -27,7 +27,7 @@ export class Guard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.adminRequired && !this.user?.admin) {
|
if (this.adminRequired && !this.user?.admin) {
|
||||||
redirect(302, '/dashboard');
|
redirect(302, '/dash');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import type { LayoutServerLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ locals: { guard } }) => {
|
|
||||||
const user = guard.requiresAuth().orRedirects().getUser();
|
|
||||||
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import IconHome from "~icons/tabler/home";
|
|
||||||
import IconLogout2 from "~icons/tabler/logout2";
|
|
||||||
import IconUsers from "~icons/tabler/users";
|
|
||||||
import IconUsersGroup from "~icons/tabler/users-group";
|
|
||||||
|
|
||||||
let { children, data } = $props();
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
name: 'Devices',
|
|
||||||
href: '/dashboard/devices',
|
|
||||||
icon: IconHome,
|
|
||||||
admin: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Users',
|
|
||||||
href: '/dashboard/users',
|
|
||||||
icon: IconUsers,
|
|
||||||
admin: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Groups',
|
|
||||||
href: '/dashboard/groups',
|
|
||||||
icon: IconUsersGroup,
|
|
||||||
admin: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex min-h-screen overflow-hidden">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div id="sidebar" class="h-screen w-64 bg-neutral-100 dark:bg-neutral-900 transform translate-x-0 transition-transform duration-300 ease-in-out z-40 text-neutral-700 dark:text-neutral-300 text-sm border-r border-neutral-200 dark:border-neutral-800">
|
|
||||||
<div class="p-4 flex flex-col h-full">
|
|
||||||
<ul class="flex-1">
|
|
||||||
{#each links as link}
|
|
||||||
{#if data.user.admin || !link.admin}
|
|
||||||
<li class="mb-2">
|
|
||||||
<a class="block flex items-center p-2 bg-transparent hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all cursor-pointer rounded" href={link.href}>
|
|
||||||
<link.icon class="mr-2"/>
|
|
||||||
{link.name}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="p-2 flex w-full justify-between items-center">
|
|
||||||
<p>Signed in as <span class="underline underline-offset-4">{data.user.name}</span></p>
|
|
||||||
<a data-sveltekit-preload-data="tap" href="/logout" class="block p-2 bg-transparent hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all cursor-pointer rounded"><IconLogout2/></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="flex-1 p-6 bg-white dark:bg-neutral-950 w-full h-screen">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { redirect, type ServerLoad } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ locals: { guard }}) => {
|
|
||||||
guard.requiresAuth().orRedirects();
|
|
||||||
redirect(302, '/dashboard/devices');
|
|
||||||
};
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { getUsersDevices } from '$lib/server/db';
|
|
||||||
import type { ServerLoad } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ locals: { guard } }) => {
|
|
||||||
const user = guard.requiresAuth().orRedirects().getUser();
|
|
||||||
return {
|
|
||||||
devices: getUsersDevices(user.id),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
|
|
||||||
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
|
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
|
||||||
import IconEdit from "~icons/tabler/edit";
|
|
||||||
import IconPlayerPlay from "~icons/tabler/player-play";
|
|
||||||
import IconPlus from "~icons/tabler/plus";
|
|
||||||
|
|
||||||
let { data } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ResourceListPage heading="Devices">
|
|
||||||
{#snippet actionSnippet()}
|
|
||||||
{#if data.user.admin}
|
|
||||||
<Button Icon={IconPlus} a href="/dashboard/devices/new">Add Device</Button>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet contentSnippet()}
|
|
||||||
{#if data.devices.length === 0}
|
|
||||||
<p>No devices found</p>
|
|
||||||
{:else}
|
|
||||||
<div class="flex gap-4 flex-wrap">
|
|
||||||
{#each data.devices as device}
|
|
||||||
<ResourceCard title={device.name} subtitle={device.mac}>
|
|
||||||
{#snippet actionsSnippet()}
|
|
||||||
{#if data.user.admin}
|
|
||||||
<Button Icon={IconEdit} a href="/dashboard/devices/{device.id}" extra="!p-2"/>
|
|
||||||
{/if}
|
|
||||||
<form method="POST" action="/dashboard/devices/{device.id}?/wake" use:enhance>
|
|
||||||
<Button Icon={IconPlayerPlay} type="submit" color="success" extra="!p-2"></Button>
|
|
||||||
</form>
|
|
||||||
{/snippet}
|
|
||||||
</ResourceCard>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</ResourceListPage>
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
import { db, getUsersDevices } from '$lib/server/db/index.js';
|
|
||||||
import { fail, redirect, type ServerLoad } from '@sveltejs/kit';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import validator from 'validator';
|
|
||||||
import { wake } from 'wake_on_lan';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
|
|
||||||
guard.requiresAdmin().orRedirects();
|
|
||||||
|
|
||||||
const device = db.data.devices.find((d) => d.id === params.slug);
|
|
||||||
|
|
||||||
if (!device && params.slug !== 'new') {
|
|
||||||
redirect(302, '/dashboard/devices');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
device,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
update: async ({ request, cookies, params, locals: { guard } }) => {
|
|
||||||
if (guard.requiresAdmin().isFailed()) return fail(403);
|
|
||||||
|
|
||||||
const form = await request.formData();
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
name: z
|
|
||||||
.string({ message: 'Name is required.' })
|
|
||||||
.min(3, { message: 'Name must be at least 3 characters.' })
|
|
||||||
.max(24, { message: 'Name must be at most 24 characters.' }),
|
|
||||||
mac: z
|
|
||||||
.string({ message: 'MAC address is required.' })
|
|
||||||
.refine((v) => validator.isMACAddress(v), { message: 'Invalid MAC address.' }),
|
|
||||||
broadcast: z
|
|
||||||
.string()
|
|
||||||
.refine((v) => validator.isIP(v), { message: 'Invalid broadcast IP address.' })
|
|
||||||
.optional(),
|
|
||||||
port: z.coerce
|
|
||||||
.number({ message: 'Port is invalid.' })
|
|
||||||
.min(1, { message: 'Port must be at least 1.' })
|
|
||||||
.max(65535, { message: 'Port must be at most 65535.' })
|
|
||||||
.optional(),
|
|
||||||
packets: z.coerce
|
|
||||||
.number({ message: 'Packets quantity is invalid.' })
|
|
||||||
.min(1, { message: 'Packets quantity must be at least 1.' })
|
|
||||||
.max(50, { message: 'Packets quantity must be at most 50.' })
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsed = schema.safeParse({
|
|
||||||
name: form.get('name')?.toString(),
|
|
||||||
mac: form.get('mac')?.toString(),
|
|
||||||
broadcast: form.get('broadcast')?.toString(),
|
|
||||||
port: form.get('port')?.toString(),
|
|
||||||
packets: form.get('packets')?.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
return fail(400, { error: parsed.error.errors[0].message });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.slug === 'new') {
|
|
||||||
await db.update(({ devices }) => {
|
|
||||||
devices.push({
|
|
||||||
id: nanoid(),
|
|
||||||
name: parsed.data.name,
|
|
||||||
mac: parsed.data.mac,
|
|
||||||
broadcast: parsed.data.broadcast ?? '255.255.255.255',
|
|
||||||
port: parsed.data.port ?? 9,
|
|
||||||
packets: parsed.data.packets ?? 3,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/devices');
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.update(({ devices }) => {
|
|
||||||
let dev = devices.find((d) => d.id === params.slug);
|
|
||||||
if (!dev) return;
|
|
||||||
|
|
||||||
dev.name = parsed.data.name;
|
|
||||||
dev.mac = parsed.data.mac;
|
|
||||||
dev.broadcast = parsed.data.broadcast ?? dev.broadcast;
|
|
||||||
dev.port = parsed.data.port ?? dev.port;
|
|
||||||
dev.packets = parsed.data.packets ?? dev.packets;
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/devices');
|
|
||||||
},
|
|
||||||
delete: async ({ locals: { guard }, params }) => {
|
|
||||||
if (guard.requiresAdmin().isFailed()) {
|
|
||||||
return fail(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.data.devices = db.data.devices.filter((d) => d.id !== params.slug);
|
|
||||||
db.data.users.forEach((u) => {
|
|
||||||
u.devices = u.devices.filter((d) => d !== params.slug);
|
|
||||||
});
|
|
||||||
db.data.groups.forEach((g) => {
|
|
||||||
g.devices = g.devices.filter((d) => d !== params.slug);
|
|
||||||
});
|
|
||||||
db.write();
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/devices');
|
|
||||||
},
|
|
||||||
wake: async ({ params, locals: { guard } }) => {
|
|
||||||
console.log('Trying to wake ' + params.slug);
|
|
||||||
|
|
||||||
guard = guard.requiresAuth();
|
|
||||||
|
|
||||||
if (guard.isFailed()) {
|
|
||||||
console.log('Failed guard');
|
|
||||||
return fail(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
const device = getUsersDevices(guard.getUser().id).find((d) => d.id === params.slug);
|
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
return fail(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Trying to wake ' + device.name);
|
|
||||||
|
|
||||||
wake(device.mac, {
|
|
||||||
address: device.broadcast,
|
|
||||||
port: device.port,
|
|
||||||
num_packets: device.packets,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import InputText from "$lib/components/forms/InputText.svelte";
|
|
||||||
import ResourceEditPage from "$lib/components/resources/ResourceEditPage.svelte";
|
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
|
||||||
import Collapsible from "$lib/components/ui/Collapsible.svelte";
|
|
||||||
import IconDeviceFloppy from "~icons/tabler/device-floppy";
|
|
||||||
import IconTrash from "~icons/tabler/trash";
|
|
||||||
|
|
||||||
let { form, data } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ResourceEditPage heading={data.device ? "Editing Device - " + data.device.name : "New Device"} error={form?.error}>
|
|
||||||
<form method="POST" action="?/update" use:enhance>
|
|
||||||
<InputText
|
|
||||||
label="Device Name"
|
|
||||||
sublabel="How the device will appear in the dashboard"
|
|
||||||
name="name"
|
|
||||||
defaultValue={data.device ? data.device.name : "New Device"}/>
|
|
||||||
|
|
||||||
<InputText
|
|
||||||
label="MAC Address"
|
|
||||||
sublabel="Address used for the Wake-On-Lan packets"
|
|
||||||
name="mac"
|
|
||||||
defaultValue={data.device ? data.device.mac : "00:00:00:00:00:00"}/>
|
|
||||||
|
|
||||||
<Collapsible label="Advanced">
|
|
||||||
<InputText
|
|
||||||
label="Broadcast IP Address"
|
|
||||||
name="broadcast"
|
|
||||||
defaultValue={data.device ? data.device.broadcast : "255.255.255.255"}/>
|
|
||||||
|
|
||||||
<InputText
|
|
||||||
label="Broadcast Port"
|
|
||||||
name="port"
|
|
||||||
defaultValue={data.device ? data.device.port : "9"}/>
|
|
||||||
|
|
||||||
<InputText
|
|
||||||
label="Packets Quantity"
|
|
||||||
name="packets"
|
|
||||||
defaultValue={data.device ? data.device.packets : "3"}/>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<div class="flex gap-5 items-center">
|
|
||||||
<Button Icon={IconDeviceFloppy}>{data.device ? "Update" : "Create"}</Button>
|
|
||||||
{#if data.device}
|
|
||||||
<Button Icon={IconTrash} color="danger" inverted formaction="?/delete">Delete</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ResourceEditPage>
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { db } from '$lib/server/db';
|
|
||||||
import { type ServerLoad } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ locals: { guard } }) => {
|
|
||||||
guard.requiresAdmin().orRedirects();
|
|
||||||
return {
|
|
||||||
groups: db.data.groups,
|
|
||||||
userCounts: db.data.groups.reduce(
|
|
||||||
(acc, group) => {
|
|
||||||
acc[group.id] = db.data.users.filter((u) => u.groups.includes(group.id)).length;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, number>,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
|
|
||||||
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
|
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
|
||||||
import IconEdit from "~icons/tabler/edit";
|
|
||||||
import IconPlus from "~icons/tabler/plus";
|
|
||||||
|
|
||||||
let { data } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ResourceListPage heading="Groups">
|
|
||||||
{#snippet actionSnippet()}
|
|
||||||
<Button Icon={IconPlus} a href="/dashboard/groups/new">Add Group</Button>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet contentSnippet()}
|
|
||||||
{#if data.groups.length === 0}
|
|
||||||
<p>No groups found</p>
|
|
||||||
{:else}
|
|
||||||
<div class="flex gap-4 flex-wrap">
|
|
||||||
{#each data.groups as group}
|
|
||||||
<ResourceCard title={group.name} subtitle="{data.userCounts[group.id]} users, {group.devices.length} devices">
|
|
||||||
{#snippet actionsSnippet()}
|
|
||||||
{#if data.user.admin}
|
|
||||||
<Button Icon={IconEdit} a href="/dashboard/groups/{group.id}" extra="!p-2"/>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</ResourceCard>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</ResourceListPage>
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import { db } from '$lib/server/db';
|
|
||||||
import { fail, redirect, type Actions, type ServerLoad } from '@sveltejs/kit';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
|
|
||||||
guard.requiresAdmin().orRedirects();
|
|
||||||
|
|
||||||
const group = db.data.groups.find((g) => g.id === params.slug);
|
|
||||||
|
|
||||||
if (!group && params.slug !== 'new') {
|
|
||||||
redirect(302, '/dashboard/groups');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
devices: db.data.devices,
|
|
||||||
group,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions: Actions = {
|
|
||||||
update: async ({ request, locals: { guard }, params }) => {
|
|
||||||
if (guard.requiresAdmin().isFailed()) return fail(403);
|
|
||||||
|
|
||||||
const form = await request.formData();
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
name: z
|
|
||||||
.string({ message: 'Name is required.' })
|
|
||||||
.min(3, { message: 'Name must be at least 3 characters.' })
|
|
||||||
.max(24, { message: 'Name must be at most 24 characters.' }),
|
|
||||||
|
|
||||||
devices: z.array(
|
|
||||||
z.string().refine((v) => db.data.devices.find((d) => d.id === v), {
|
|
||||||
message: 'Invalid device ID.',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsed = schema.safeParse({
|
|
||||||
name: form.get('name'),
|
|
||||||
devices: form.getAll('devices'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
return fail(400, { error: parsed.error.errors[0].message });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.slug === 'new') {
|
|
||||||
await db.update(({ groups }) => {
|
|
||||||
groups.push({
|
|
||||||
id: nanoid(),
|
|
||||||
name: parsed.data.name,
|
|
||||||
devices: parsed.data.devices,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/groups');
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.update(({ groups }) => {
|
|
||||||
let group = groups.find((g) => g.id === params.slug);
|
|
||||||
if (!group) return;
|
|
||||||
|
|
||||||
group.name = parsed.data.name;
|
|
||||||
group.devices = parsed.data.devices;
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/groups');
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async ({ locals: { guard }, params }) => {
|
|
||||||
if (guard.requiresAdmin().isFailed()) {
|
|
||||||
return fail(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.data.groups = db.data.groups.filter((g) => g.id !== params.slug);
|
|
||||||
db.data.users.forEach((u) => {
|
|
||||||
u.groups = u.groups.filter((g) => g !== params.slug);
|
|
||||||
});
|
|
||||||
db.write();
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/groups');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import InputSelect from "$lib/components/forms/InputSelect.svelte";
|
|
||||||
import InputText from "$lib/components/forms/InputText.svelte";
|
|
||||||
import ResourceEditPage from "$lib/components/resources/ResourceEditPage.svelte";
|
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
|
||||||
import IconDeviceFloppy from "~icons/tabler/device-floppy";
|
|
||||||
import IconTrash from "~icons/tabler/trash";
|
|
||||||
|
|
||||||
let { form, data } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ResourceEditPage heading={data.group ? "Editing Group - " + data.group.name : "New Group"} error={form?.error}>
|
|
||||||
<form method="POST" action="?/update" use:enhance>
|
|
||||||
<InputText
|
|
||||||
label="Group Name"
|
|
||||||
sublabel="How the group will appear in the dashboard"
|
|
||||||
name="name"
|
|
||||||
defaultValue={data.group ? data.group.name : "New Group"}/>
|
|
||||||
|
|
||||||
<InputSelect
|
|
||||||
label="Devices"
|
|
||||||
sublabel="Devices that users who are part of this group will be ble to wake"
|
|
||||||
name="devices"
|
|
||||||
data={data.devices.map(d => ({
|
|
||||||
value: d.id.toString(),
|
|
||||||
name: d.name,
|
|
||||||
selected: data.group?.devices.find(gd => gd === d.id) ? true : false }))}/>
|
|
||||||
|
|
||||||
<div class="flex gap-5 items-center">
|
|
||||||
<Button Icon={IconDeviceFloppy}>{data.group ? "Update" : "Create"}</Button>
|
|
||||||
{#if data.group}
|
|
||||||
<Button Icon={IconTrash} color="danger" inverted formaction="?/delete">Delete</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ResourceEditPage>
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { db } from '$lib/server/db';
|
|
||||||
import type { User } from '$lib/server/db/types/user';
|
|
||||||
import type { ServerLoad } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ locals: { guard } }) => {
|
|
||||||
guard.requiresAdmin().orRedirects();
|
|
||||||
|
|
||||||
return {
|
|
||||||
users: db.data.users.map((u) => {
|
|
||||||
let safeUser = structuredClone(u) as Partial<User>;
|
|
||||||
delete safeUser['password'];
|
|
||||||
|
|
||||||
return safeUser;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
|
|
||||||
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
|
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
|
||||||
import IconEdit from "~icons/tabler/edit";
|
|
||||||
import IconPlus from "~icons/tabler/plus";
|
|
||||||
|
|
||||||
let { data } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ResourceListPage heading="Users">
|
|
||||||
{#snippet actionSnippet()}
|
|
||||||
<Button Icon={IconPlus} a href="/dashboard/users/new">Add User</Button>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet contentSnippet()}
|
|
||||||
<div class="flex gap-4 flex-wrap">
|
|
||||||
{#each data.users as user}
|
|
||||||
<ResourceCard title={user.name} subtitle="{user.groups!.length} groups, {user.devices!.length} devices">
|
|
||||||
{#snippet actionsSnippet()}
|
|
||||||
<Button Icon={IconEdit} a href="/dashboard/users/{user.id}" extra="!p-2"/>
|
|
||||||
{/snippet}
|
|
||||||
</ResourceCard>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</ResourceListPage>
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
import { db } from '$lib/server/db';
|
|
||||||
import type { User } from '$lib/server/db/types/user.js';
|
|
||||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const load = async ({ locals: { guard }, params }) => {
|
|
||||||
guard.requiresAdmin().orRedirects();
|
|
||||||
|
|
||||||
let user = db.data.users.find((u) => u.id === params.slug) as Partial<User>;
|
|
||||||
|
|
||||||
if (!user && params.slug !== 'new') {
|
|
||||||
redirect(302, '/dashboard/users');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
user = structuredClone(user);
|
|
||||||
delete user['password'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
groups: db.data.groups,
|
|
||||||
devices: db.data.devices,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions: Actions = {
|
|
||||||
update: async ({ request, locals: { guard }, params }) => {
|
|
||||||
if (guard.requiresAdmin().isFailed()) return fail(403);
|
|
||||||
|
|
||||||
const form = await request.formData();
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
name: z
|
|
||||||
.string({ message: 'Name is required.' })
|
|
||||||
.min(3, { message: 'Name must be at least 3 characters.' })
|
|
||||||
.max(24, { message: 'Name must be at most 24 characters.' }),
|
|
||||||
admin: z.boolean(),
|
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.refine((v) => (params.slug === 'new' ? v && v.length > 0 : true), {
|
|
||||||
message: 'Password is required at user creation.',
|
|
||||||
})
|
|
||||||
.refine((v) => !v || v.length >= 8, {
|
|
||||||
message: 'Password must be at least 8 characters.',
|
|
||||||
}),
|
|
||||||
groups: z.array(
|
|
||||||
z.string().refine((v) => db.data.groups.find((g) => g.id === v), {
|
|
||||||
message: 'Invalid group ID.',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
devices: z.array(
|
|
||||||
z.string().refine((v) => db.data.devices.find((d) => d.id === v), {
|
|
||||||
message: 'Invalid device ID.',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsed = schema.safeParse({
|
|
||||||
name: form.get('name'),
|
|
||||||
admin: form.get('admin') === 'on',
|
|
||||||
password: form.get('password'),
|
|
||||||
groups: form.getAll('groups'),
|
|
||||||
devices: form.getAll('devices'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
return fail(400, { error: parsed.error.errors[0].message });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.slug === 'new') {
|
|
||||||
await db.update(({ users }) => {
|
|
||||||
users.push({
|
|
||||||
id: nanoid(),
|
|
||||||
name: parsed.data.name,
|
|
||||||
admin: parsed.data.admin,
|
|
||||||
groups: parsed.data.groups,
|
|
||||||
devices: parsed.data.devices,
|
|
||||||
password: bcrypt.hashSync(parsed.data.password!, 10),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/users');
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.update(({ users }) => {
|
|
||||||
let user = users.find((u) => u.id === params.slug);
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
user.name = parsed.data.name;
|
|
||||||
user.admin = parsed.data.admin;
|
|
||||||
user.groups = parsed.data.groups;
|
|
||||||
user.devices = parsed.data.devices;
|
|
||||||
|
|
||||||
if (parsed.data.password && parsed.data.password.length > 0) {
|
|
||||||
user.password = bcrypt.hashSync(parsed.data.password, 10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect(302, '/dashboard/users');
|
|
||||||
},
|
|
||||||
delete: async ({ locals: { guard }, params }) => {
|
|
||||||
if (guard.requiresAdmin().isFailed()) {
|
|
||||||
return fail(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.data.users = db.data.users.filter((u) => u.id !== params.slug);
|
|
||||||
db.write();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
import InputSelect from "$lib/components/forms/InputSelect.svelte";
|
|
||||||
import InputText from "$lib/components/forms/InputText.svelte";
|
|
||||||
import InputToggle from "$lib/components/forms/InputToggle.svelte";
|
|
||||||
import ResourceEditPage from "$lib/components/resources/ResourceEditPage.svelte";
|
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
|
||||||
import IconDeviceFloppy from "~icons/tabler/device-floppy";
|
|
||||||
import IconTrash from "~icons/tabler/trash";
|
|
||||||
|
|
||||||
let { data, form } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ResourceEditPage heading={data.user ? "Editing User - " + data.user.name : "New User"} error={form?.error}>
|
|
||||||
<form method="POST" action="?/update" use:enhance>
|
|
||||||
<InputText
|
|
||||||
label="User Name"
|
|
||||||
sublabel="How the user will appear in the dashboard"
|
|
||||||
name="name"
|
|
||||||
defaultValue={data.user ? data.user.name : "New User"}/>
|
|
||||||
|
|
||||||
<InputText
|
|
||||||
label="Password"
|
|
||||||
sublabel="Leave empty for no change"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
defaultValue=""/>
|
|
||||||
|
|
||||||
<InputToggle
|
|
||||||
label="Admin"
|
|
||||||
name="admin"
|
|
||||||
sublabel="Can manage and use all devices, groups and users"
|
|
||||||
checked={data.user?.admin ?? false}/>
|
|
||||||
|
|
||||||
<InputSelect
|
|
||||||
label="Groups"
|
|
||||||
name="groups"
|
|
||||||
sublabel="Groups that the user will be a part of and inherit devices access from"
|
|
||||||
data={data.groups.map(g => ({
|
|
||||||
value: g.id.toString(),
|
|
||||||
name: g.name,
|
|
||||||
selected: data.user?.groups?.find(ug => ug === g.id) ? true : false }))}/>
|
|
||||||
|
|
||||||
<InputSelect
|
|
||||||
label="Devices"
|
|
||||||
name="devices"
|
|
||||||
sublabel="Devices that the user can wake"
|
|
||||||
data={data.devices.map(d => ({
|
|
||||||
value: d.id.toString(),
|
|
||||||
name: d.name,
|
|
||||||
selected: data.user?.devices?.find(ud => ud === d.id) ? true : false }))}/>
|
|
||||||
|
|
||||||
<div class="flex gap-5 items-center">
|
|
||||||
<Button Icon={IconDeviceFloppy}>{data.user ? "Update" : "Create"}</Button>
|
|
||||||
{#if data.user}
|
|
||||||
<Button Icon={IconTrash} color="danger" inverted formaction="?/delete">Delete</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ResourceEditPage>
|
|
||||||
@ -6,9 +6,9 @@ import type { Actions } from './$types';
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ cookies, request }) => {
|
default: async ({ cookies, request, locals: { guard } }) => {
|
||||||
if (getUserFromSession(cookies.get('session'))) {
|
if (!guard.requiresAuth().isFailed()) {
|
||||||
redirect(302, '/dashboard');
|
redirect(302, '/dash');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user