added most features required + better auth
This commit is contained in:
parent
bf114411ff
commit
c753171846
7
src/app.d.ts
vendored
7
src/app.d.ts
vendored
@ -1,9 +1,14 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
|
||||
import type { Guard } from "$lib/server/guard";
|
||||
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
interface Locals {
|
||||
guard: Guard;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
|
||||
@ -3,6 +3,8 @@ import type { ServerInit } from "@sveltejs/kit";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { nanoid } from "nanoid";
|
||||
import { writeFileSync } from "fs";
|
||||
import { Guard } from "$lib/server/guard";
|
||||
import { getUserFromSession } from "$lib/server/sessions";
|
||||
|
||||
export const init: ServerInit = async () => {
|
||||
const anyUser = db.data.users.at(0);
|
||||
@ -26,4 +28,12 @@ export const init: ServerInit = async () => {
|
||||
|
||||
writeFileSync("./data/default_admin_pass.txt", pass);
|
||||
}
|
||||
};
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
const { cookies, locals } = event;
|
||||
|
||||
locals.guard = new Guard(await getUserFromSession(cookies.get("session")));
|
||||
|
||||
return await resolve(event);
|
||||
};
|
||||
@ -6,6 +6,6 @@ export type User = {
|
||||
name: string,
|
||||
password: string,
|
||||
admin: boolean
|
||||
groups: Group[],
|
||||
groups: string[],
|
||||
permissions: { [key: string]: Permission }
|
||||
}
|
||||
51
src/lib/server/guard.ts
Normal file
51
src/lib/server/guard.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import type { User } from "./db/types/user";
|
||||
|
||||
export class Guard {
|
||||
private readonly user?: User;
|
||||
|
||||
private authRequired = false;
|
||||
private adminRequired = false;
|
||||
|
||||
constructor(user?: User, options?: { authRequired?: boolean, adminRequired?: boolean }) {
|
||||
this.user = user;
|
||||
this.authRequired = options?.authRequired ?? false;
|
||||
this.adminRequired = options?.adminRequired ?? false;
|
||||
}
|
||||
|
||||
public requiresAuth() {
|
||||
return new Guard(this.user, { authRequired: true });
|
||||
}
|
||||
|
||||
public requiresAdmin() {
|
||||
return new Guard(this.user, { authRequired: true, adminRequired: true });
|
||||
}
|
||||
|
||||
public orRedirects() {
|
||||
if (this.authRequired && !this.user) {
|
||||
redirect(302, "/login");
|
||||
}
|
||||
|
||||
if (this.adminRequired && !this.user?.admin) {
|
||||
redirect(302, "/dashboard");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public isFailed() {
|
||||
if (this.authRequired && !this.user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.adminRequired && !this.user?.admin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getUser(): User {
|
||||
return this.user!;
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,7 @@
|
||||
import { getUserFromSession } from "$lib/server/sessions";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { LayoutServerLoad } from "./$types";
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
const user = await getUserFromSession(cookies.get("session"));
|
||||
|
||||
if (!user) {
|
||||
redirect(302, "/login");
|
||||
}
|
||||
export const load: LayoutServerLoad = async ({ locals: { guard } }) => {
|
||||
const user = guard.requiresAuth().orRedirects().getUser();
|
||||
|
||||
return {
|
||||
user,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
</script>
|
||||
|
||||
<p>logged in as {data.user.name}</p>
|
||||
<a href="/dashboard">home</a>
|
||||
<a href="/dashboard/devices">devices</a>
|
||||
{#if data.user.admin}
|
||||
<a href="/dashboard/users">users</a>
|
||||
<a href="/dashboard/groups">groups</a>
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
import { db } from "$lib/server/db/db";
|
||||
import type { User } from "$lib/server/db/types/user";
|
||||
import type { ServerLoad } from "@sveltejs/kit";
|
||||
import { redirect, type ServerLoad } from "@sveltejs/kit";
|
||||
|
||||
export const load: ServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent() as { user: User };
|
||||
|
||||
return {
|
||||
devices: user.admin ? db.data.devices :
|
||||
db.data.devices.filter(device =>
|
||||
Object.keys(user.permissions).includes(device.id) ||
|
||||
user.groups.some(group => Object.keys(group.permissions).includes(device.id))
|
||||
),
|
||||
}
|
||||
export const load: ServerLoad = async ({ locals: { guard }}) => {
|
||||
guard.requiresAuth().orRedirects();
|
||||
redirect(302, '/dashboard/devices');
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
aaaa
|
||||
17
src/routes/dashboard/devices/+page.server.ts
Normal file
17
src/routes/dashboard/devices/+page.server.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { db } from "$lib/server/db/db";
|
||||
import type { ServerLoad } from "@sveltejs/kit";
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard} }) => {
|
||||
const user = guard.requiresAuth().orRedirects().getUser();
|
||||
|
||||
return {
|
||||
devices: user.admin ? db.data.devices :
|
||||
db.data.devices.filter(device =>
|
||||
Object.keys(user.permissions).includes(device.id) ||
|
||||
user.groups.some(groupId => {
|
||||
const group = db.data.groups.find(group => group.id === groupId);
|
||||
return group && Object.keys(group.permissions).includes(device.id)
|
||||
})
|
||||
),
|
||||
}
|
||||
};
|
||||
18
src/routes/dashboard/devices/+page.svelte
Normal file
18
src/routes/dashboard/devices/+page.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
{#if data.user.admin}
|
||||
<a href="/dashboard/devices/new">new device</a>
|
||||
{/if}
|
||||
|
||||
{#if data.devices.length === 0}
|
||||
<p>No devices found</p>
|
||||
{:else}
|
||||
<p>Devices:</p>
|
||||
<ul>
|
||||
{#each data.devices as device}
|
||||
<li>{device.name} {#if data.user.admin}- <a href="/dashboard/devices/{device.id}">edit</a>{/if}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
86
src/routes/dashboard/devices/[slug]/+page.server.ts
Normal file
86
src/routes/dashboard/devices/[slug]/+page.server.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { db } from '$lib/server/db/db';
|
||||
import { getUserFromSession } from '$lib/server/sessions';
|
||||
import { fail, redirect, type ServerLoad } from '@sveltejs/kit';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
const device = db.data.devices.find(device => device.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 name = form.get("name")?.toString();
|
||||
const mac = form.get("mac")?.toString();
|
||||
const ip = form.get("ip")?.toString();
|
||||
const port = form.get("port")?.toString();
|
||||
const packets = form.get("packets")?.toString();
|
||||
|
||||
if (!name || !mac || !ip || !port || !packets) {
|
||||
// TODO better validation
|
||||
return {
|
||||
error: "MISSING_FIELDS"
|
||||
}
|
||||
}
|
||||
|
||||
if (params.slug === "new") {
|
||||
await db.update(({ devices }) => {
|
||||
devices.push({
|
||||
id: nanoid(),
|
||||
name,
|
||||
mac,
|
||||
ip,
|
||||
port: parseInt(port),
|
||||
packets: parseInt(packets)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let device = db.data.devices.find(device => device.id === params.slug);
|
||||
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
device.name = name;
|
||||
device.mac = mac;
|
||||
device.ip = ip;
|
||||
device.port = parseInt(port);
|
||||
device.packets = parseInt(packets);
|
||||
|
||||
await db.write();
|
||||
}
|
||||
|
||||
redirect(302, "/dashboard/devices");
|
||||
},
|
||||
delete: async ({ locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) {
|
||||
return fail(403);
|
||||
}
|
||||
|
||||
db.data.devices = db.data.devices.filter(device => device.id !== params.slug);
|
||||
db.data.users.forEach(user => {
|
||||
delete user.permissions[params.slug];
|
||||
});
|
||||
db.data.groups.forEach(group => {
|
||||
delete group.permissions[params.slug];
|
||||
});
|
||||
|
||||
await db.write();
|
||||
|
||||
redirect(302, "/dashboard/devices");
|
||||
}
|
||||
}
|
||||
35
src/routes/dashboard/devices/[slug]/+page.svelte
Normal file
35
src/routes/dashboard/devices/[slug]/+page.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
let { form, data } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/update">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" type="text" defaultValue={data.device ? data.device.name : "New Device"}>
|
||||
</label>
|
||||
<label>
|
||||
Mac Address
|
||||
<input name="mac" type="text" defaultValue={data.device ? data.device.mac : "00:00:00:00:00:00"}>
|
||||
</label>
|
||||
<label>
|
||||
Broadcast Ip Address
|
||||
<input name="ip" type="text" defaultValue={data.device ? data.device.ip : "255.255.255.255"}>
|
||||
</label>
|
||||
<label>
|
||||
Port
|
||||
<input name="port" type="text" defaultValue={data.device ? data.device.port : "9"}>
|
||||
</label>
|
||||
<label>
|
||||
Amount of packets
|
||||
<input name="packets" type="text" defaultValue={data.device ? data.device.packets : "3"}>
|
||||
</label>
|
||||
|
||||
<button>{data.device ? "Update" : "Create"}</button>
|
||||
{#if data.device}
|
||||
<button formaction="?/delete">Delete</button>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
{#if form?.error}
|
||||
<p>Could not create device: {form.error}</p>
|
||||
{/if}
|
||||
10
src/routes/dashboard/groups/+page.server.ts
Normal file
10
src/routes/dashboard/groups/+page.server.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { db } from "$lib/server/db/db";
|
||||
import { type ServerLoad } from "@sveltejs/kit";
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard } }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
return {
|
||||
groups: db.data.groups,
|
||||
}
|
||||
};
|
||||
16
src/routes/dashboard/groups/+page.svelte
Normal file
16
src/routes/dashboard/groups/+page.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<a href="/dashboard/groups/new">new group</a>
|
||||
|
||||
{#if data.groups.length === 0}
|
||||
<p>No groups found</p>
|
||||
{:else}
|
||||
<p>Groups:</p>
|
||||
<ul>
|
||||
{#each data.groups as group}
|
||||
<li>{group.name} - <a href="/dashboard/groups/{group.id}">edit</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
92
src/routes/dashboard/groups/[slug]/+page.server.ts
Normal file
92
src/routes/dashboard/groups/[slug]/+page.server.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { db } from "$lib/server/db/db";
|
||||
import type { Permission } from "$lib/server/db/types/permission";
|
||||
import type { User } from "$lib/server/db/types/user";
|
||||
import { getUserFromSession } from "$lib/server/sessions";
|
||||
import { fail, redirect, type Actions, type ServerLoad } from "@sveltejs/kit";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard },params }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
const group = db.data.groups.find(group => group.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 name = form.get("name")?.toString();
|
||||
|
||||
let permissions: { [key: string]: Permission } = {};
|
||||
|
||||
for (let deviceId of form.getAll("canSee")) {
|
||||
if (db.data.devices.find(device => device.id === deviceId)) {
|
||||
permissions[deviceId.toString()] = { wake: false };
|
||||
}
|
||||
}
|
||||
|
||||
for (let deviceId of form.getAll("canWake")) {
|
||||
if (db.data.devices.find(device => device.id === deviceId)) {
|
||||
permissions[deviceId.toString()] = { wake: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
// TODO better validation
|
||||
return {
|
||||
error: "MISSING_FIELDS"
|
||||
}
|
||||
}
|
||||
|
||||
if (params.slug === "new") {
|
||||
await db.update(({ groups }) => {
|
||||
groups.push({
|
||||
id: nanoid(),
|
||||
name,
|
||||
permissions,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await db.update(({ groups }) => {
|
||||
const group = groups.find(group => group.id === params.slug);
|
||||
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
group.name = name;
|
||||
group.permissions = permissions;
|
||||
});
|
||||
}
|
||||
|
||||
redirect(302, "/dashboard/groups");
|
||||
},
|
||||
|
||||
delete: async ({ locals: { guard }, params }) => {
|
||||
if (guard.requiresAdmin().isFailed()) {
|
||||
return fail(403);
|
||||
}
|
||||
|
||||
db.data.groups = db.data.groups.filter(group => group.id !== params.slug);
|
||||
db.data.users = db.data.users.map(user => {
|
||||
user.groups = user.groups.filter(groupId => groupId !== params.slug);
|
||||
return user;
|
||||
});
|
||||
|
||||
await db.write();
|
||||
|
||||
redirect(302, "/dashboard/groups");
|
||||
}
|
||||
};
|
||||
34
src/routes/dashboard/groups/[slug]/+page.svelte
Normal file
34
src/routes/dashboard/groups/[slug]/+page.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
let { form, data } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/update">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" type="text" defaultValue={data.group ? data.group.name : "New Group"}>
|
||||
</label>
|
||||
<label>
|
||||
Can see
|
||||
<select multiple name="canSee">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.group?.permissions[device.id] ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Can wake
|
||||
<select multiple name="canWake">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.group?.permissions[device.id]?.wake ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button>{data.group ? "Update" : "Create"}</button>
|
||||
{#if data.group}
|
||||
<button formaction="?/delete">Delete</button>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
{#if form?.error}
|
||||
<p>Could not {data.group ? "update" : "create"} group: {form.error}</p>
|
||||
{/if}
|
||||
10
src/routes/dashboard/users/+page.server.ts
Normal file
10
src/routes/dashboard/users/+page.server.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { db } from "$lib/server/db/db";
|
||||
import type { ServerLoad } from "@sveltejs/kit";
|
||||
|
||||
export const load: ServerLoad = async ({ locals: { guard } }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
return {
|
||||
users: db.data.users,
|
||||
}
|
||||
};
|
||||
16
src/routes/dashboard/users/+page.svelte
Normal file
16
src/routes/dashboard/users/+page.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<a href="/dashboard/users/new">new user</a>
|
||||
|
||||
{#if data.users.length === 0}
|
||||
<p>No users found</p>
|
||||
{:else}
|
||||
<p>Users:</p>
|
||||
<ul>
|
||||
{#each data.users as user}
|
||||
<li>{user.name} - <a href="/dashboard/users/{user.id}">edit</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
90
src/routes/dashboard/users/[slug]/+page.server.ts
Normal file
90
src/routes/dashboard/users/[slug]/+page.server.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { db } from "$lib/server/db/db";
|
||||
import type { Permission } from "$lib/server/db/types/permission";
|
||||
import { fail, redirect, type Actions } from "@sveltejs/kit";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export const load = async ({ locals: { guard }, params }) => {
|
||||
guard.requiresAdmin().orRedirects();
|
||||
|
||||
const user = db.data.users.find(user => user.id === params.slug);
|
||||
|
||||
if (!user && params.slug !== "new") {
|
||||
redirect(302, "/dashboard/users");
|
||||
}
|
||||
|
||||
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 name = form.get("name")?.toString();
|
||||
const admin = form.get("admin")?.toString() === "on";
|
||||
const password = form.get("password")?.toString() ?? "";
|
||||
const groups = form.getAll("groups")
|
||||
.map(groupId => groupId.toString())
|
||||
.filter(groupId => db.data.groups.find(group => group.id === groupId));
|
||||
|
||||
let permissions: { [key: string]: Permission } = {};
|
||||
|
||||
for (let deviceId of form.getAll("canSee")) {
|
||||
if (db.data.devices.find(device => device.id === deviceId)) {
|
||||
permissions[deviceId.toString()] = { wake: false };
|
||||
}
|
||||
}
|
||||
|
||||
for (let deviceId of form.getAll("canWake")) {
|
||||
if (db.data.devices.find(device => device.id === deviceId)) {
|
||||
permissions[deviceId.toString()] = { wake: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
// TODO better validation
|
||||
return {
|
||||
error: "MISSING_FIELDS"
|
||||
}
|
||||
}
|
||||
|
||||
if (params.slug === "new") {
|
||||
if (password.length < 8) {
|
||||
return {
|
||||
error: "PASSWORD_TOO_WEAK"
|
||||
}
|
||||
}
|
||||
|
||||
await db.update(({ users }) => {
|
||||
users.push({
|
||||
id: nanoid(),
|
||||
name,
|
||||
password: bcrypt.hashSync(password, 10),
|
||||
admin,
|
||||
groups,
|
||||
permissions
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await db.update(({ users }) => {
|
||||
const user = users.find(user => user.id === params.slug);
|
||||
if (user) {
|
||||
user.name = name;
|
||||
user.admin = admin;
|
||||
if (password !== "") {
|
||||
user.password = bcrypt.hashSync(password, 10);
|
||||
}
|
||||
user.groups = groups;
|
||||
user.permissions = permissions;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
50
src/routes/dashboard/users/[slug]/+page.svelte
Normal file
50
src/routes/dashboard/users/[slug]/+page.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
let { data, form } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/update">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" type="text" defaultValue={data.user ? data.user.name : "New User"}>
|
||||
</label>
|
||||
<label>
|
||||
Password (leave empty for no change)
|
||||
<input name="password" type="password">
|
||||
</label>
|
||||
<label>
|
||||
Is admin
|
||||
<input name="admin" type="checkbox" checked={data.user ? data.user.admin : false}>
|
||||
</label>
|
||||
<label>
|
||||
Groups
|
||||
<select multiple name="groups">
|
||||
{#each data.groups as group}
|
||||
<option value={group.id} selected={data.user?.groups.includes(group.id) ? true : false}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Can see
|
||||
<select multiple name="canSee">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.user?.permissions[device.id] ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Can wake
|
||||
<select multiple name="canWake">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.user?.permissions[device.id]?.wake ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button>{data.user ? "Update" : "Create"}</button>
|
||||
{#if data.user}
|
||||
<button formaction="?/delete">Delete</button>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
{#if form?.error}
|
||||
<p>Could not update user: {form.error}</p>
|
||||
{/if}
|
||||
Loading…
x
Reference in New Issue
Block a user