From c753171846d70fd87f7c4a9819bcbf81f4cd067e Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 8 Apr 2025 01:42:24 +0200 Subject: [PATCH] added most features required + better auth --- src/app.d.ts | 7 +- src/hooks.server.ts | 10 ++ src/lib/server/db/types/user.ts | 2 +- src/lib/server/guard.ts | 51 ++++++++++ src/routes/dashboard/+layout.server.ts | 10 +- src/routes/dashboard/+layout.svelte | 2 +- src/routes/dashboard/+page.server.ts | 17 +--- src/routes/dashboard/+page.svelte | 5 - src/routes/dashboard/devices/+page.server.ts | 17 ++++ src/routes/dashboard/devices/+page.svelte | 18 ++++ .../dashboard/devices/[slug]/+page.server.ts | 86 +++++++++++++++++ .../dashboard/devices/[slug]/+page.svelte | 35 +++++++ src/routes/dashboard/groups/+page.server.ts | 10 ++ src/routes/dashboard/groups/+page.svelte | 16 ++++ .../dashboard/groups/[slug]/+page.server.ts | 92 +++++++++++++++++++ .../dashboard/groups/[slug]/+page.svelte | 34 +++++++ src/routes/dashboard/users/+page.server.ts | 10 ++ src/routes/dashboard/users/+page.svelte | 16 ++++ .../dashboard/users/[slug]/+page.server.ts | 90 ++++++++++++++++++ .../dashboard/users/[slug]/+page.svelte | 50 ++++++++++ 20 files changed, 549 insertions(+), 29 deletions(-) create mode 100644 src/lib/server/guard.ts delete mode 100644 src/routes/dashboard/+page.svelte create mode 100644 src/routes/dashboard/devices/+page.server.ts create mode 100644 src/routes/dashboard/devices/+page.svelte create mode 100644 src/routes/dashboard/devices/[slug]/+page.server.ts create mode 100644 src/routes/dashboard/devices/[slug]/+page.svelte create mode 100644 src/routes/dashboard/groups/+page.server.ts create mode 100644 src/routes/dashboard/groups/+page.svelte create mode 100644 src/routes/dashboard/groups/[slug]/+page.server.ts create mode 100644 src/routes/dashboard/groups/[slug]/+page.svelte create mode 100644 src/routes/dashboard/users/+page.server.ts create mode 100644 src/routes/dashboard/users/+page.svelte create mode 100644 src/routes/dashboard/users/[slug]/+page.server.ts create mode 100644 src/routes/dashboard/users/[slug]/+page.svelte diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..3c856c0 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -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 {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 7aef4d9..2dd97ee 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -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); }; \ No newline at end of file diff --git a/src/lib/server/db/types/user.ts b/src/lib/server/db/types/user.ts index 18ee6dd..e738a2d 100644 --- a/src/lib/server/db/types/user.ts +++ b/src/lib/server/db/types/user.ts @@ -6,6 +6,6 @@ export type User = { name: string, password: string, admin: boolean - groups: Group[], + groups: string[], permissions: { [key: string]: Permission } } \ No newline at end of file diff --git a/src/lib/server/guard.ts b/src/lib/server/guard.ts new file mode 100644 index 0000000..44fe3ae --- /dev/null +++ b/src/lib/server/guard.ts @@ -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!; + } +} \ No newline at end of file diff --git a/src/routes/dashboard/+layout.server.ts b/src/routes/dashboard/+layout.server.ts index 278c7e5..3446805 100644 --- a/src/routes/dashboard/+layout.server.ts +++ b/src/routes/dashboard/+layout.server.ts @@ -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, diff --git a/src/routes/dashboard/+layout.svelte b/src/routes/dashboard/+layout.svelte index 6af5864..adddfdb 100644 --- a/src/routes/dashboard/+layout.svelte +++ b/src/routes/dashboard/+layout.svelte @@ -3,7 +3,7 @@

logged in as {data.user.name}

-home +devices {#if data.user.admin} users groups diff --git a/src/routes/dashboard/+page.server.ts b/src/routes/dashboard/+page.server.ts index 4c12f78..328f836 100644 --- a/src/routes/dashboard/+page.server.ts +++ b/src/routes/dashboard/+page.server.ts @@ -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'); }; \ No newline at end of file diff --git a/src/routes/dashboard/+page.svelte b/src/routes/dashboard/+page.svelte deleted file mode 100644 index bbf4a0b..0000000 --- a/src/routes/dashboard/+page.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -aaaa \ No newline at end of file diff --git a/src/routes/dashboard/devices/+page.server.ts b/src/routes/dashboard/devices/+page.server.ts new file mode 100644 index 0000000..477dd06 --- /dev/null +++ b/src/routes/dashboard/devices/+page.server.ts @@ -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) + }) + ), + } +}; \ No newline at end of file diff --git a/src/routes/dashboard/devices/+page.svelte b/src/routes/dashboard/devices/+page.svelte new file mode 100644 index 0000000..6637499 --- /dev/null +++ b/src/routes/dashboard/devices/+page.svelte @@ -0,0 +1,18 @@ + + +{#if data.user.admin} + new device +{/if} + +{#if data.devices.length === 0} +

No devices found

+{:else} +

Devices:

+ +{/if} \ No newline at end of file diff --git a/src/routes/dashboard/devices/[slug]/+page.server.ts b/src/routes/dashboard/devices/[slug]/+page.server.ts new file mode 100644 index 0000000..2fdf03d --- /dev/null +++ b/src/routes/dashboard/devices/[slug]/+page.server.ts @@ -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"); + } +} \ No newline at end of file diff --git a/src/routes/dashboard/devices/[slug]/+page.svelte b/src/routes/dashboard/devices/[slug]/+page.svelte new file mode 100644 index 0000000..c7ea642 --- /dev/null +++ b/src/routes/dashboard/devices/[slug]/+page.svelte @@ -0,0 +1,35 @@ + + +
+ + + + + + + + {#if data.device} + + {/if} +
+ +{#if form?.error} +

Could not create device: {form.error}

+{/if} \ No newline at end of file diff --git a/src/routes/dashboard/groups/+page.server.ts b/src/routes/dashboard/groups/+page.server.ts new file mode 100644 index 0000000..ff3813e --- /dev/null +++ b/src/routes/dashboard/groups/+page.server.ts @@ -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, + } +}; \ No newline at end of file diff --git a/src/routes/dashboard/groups/+page.svelte b/src/routes/dashboard/groups/+page.svelte new file mode 100644 index 0000000..7ad42ae --- /dev/null +++ b/src/routes/dashboard/groups/+page.svelte @@ -0,0 +1,16 @@ + + +new group + +{#if data.groups.length === 0} +

No groups found

+{:else} +

Groups:

+ +{/if} \ No newline at end of file diff --git a/src/routes/dashboard/groups/[slug]/+page.server.ts b/src/routes/dashboard/groups/[slug]/+page.server.ts new file mode 100644 index 0000000..6b0fe4d --- /dev/null +++ b/src/routes/dashboard/groups/[slug]/+page.server.ts @@ -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"); + } +}; \ No newline at end of file diff --git a/src/routes/dashboard/groups/[slug]/+page.svelte b/src/routes/dashboard/groups/[slug]/+page.svelte new file mode 100644 index 0000000..d4b2eef --- /dev/null +++ b/src/routes/dashboard/groups/[slug]/+page.svelte @@ -0,0 +1,34 @@ + + +
+ + + + + {#if data.group} + + {/if} +
+ +{#if form?.error} +

Could not {data.group ? "update" : "create"} group: {form.error}

+{/if} \ No newline at end of file diff --git a/src/routes/dashboard/users/+page.server.ts b/src/routes/dashboard/users/+page.server.ts new file mode 100644 index 0000000..f8b5abb --- /dev/null +++ b/src/routes/dashboard/users/+page.server.ts @@ -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, + } +}; \ No newline at end of file diff --git a/src/routes/dashboard/users/+page.svelte b/src/routes/dashboard/users/+page.svelte new file mode 100644 index 0000000..ea48b7f --- /dev/null +++ b/src/routes/dashboard/users/+page.svelte @@ -0,0 +1,16 @@ + + +new user + +{#if data.users.length === 0} +

No users found

+{:else} +

Users:

+ +{/if} \ No newline at end of file diff --git a/src/routes/dashboard/users/[slug]/+page.server.ts b/src/routes/dashboard/users/[slug]/+page.server.ts new file mode 100644 index 0000000..196415e --- /dev/null +++ b/src/routes/dashboard/users/[slug]/+page.server.ts @@ -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; + } + }); + } + } +}; \ No newline at end of file diff --git a/src/routes/dashboard/users/[slug]/+page.svelte b/src/routes/dashboard/users/[slug]/+page.svelte new file mode 100644 index 0000000..2da0a2c --- /dev/null +++ b/src/routes/dashboard/users/[slug]/+page.svelte @@ -0,0 +1,50 @@ + + +
+ + + + + + + + {#if data.user} + + {/if} +
+ +{#if form?.error} +

Could not update user: {form.error}

+{/if} \ No newline at end of file