added most features required + better auth

This commit is contained in:
axel 2025-04-08 01:42:24 +02:00
parent bf114411ff
commit c753171846
20 changed files with 549 additions and 29 deletions

7
src/app.d.ts vendored
View File

@ -1,9 +1,14 @@
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
import type { Guard } from "$lib/server/guard";
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} interface Locals {
guard: Guard;
}
// interface PageData {} // interface PageData {}
// interface PageState {} // interface PageState {}
// interface Platform {} // interface Platform {}

View File

@ -3,6 +3,8 @@ import type { ServerInit } from "@sveltejs/kit";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { writeFileSync } from "fs"; import { writeFileSync } from "fs";
import { Guard } from "$lib/server/guard";
import { getUserFromSession } from "$lib/server/sessions";
export const init: ServerInit = async () => { export const init: ServerInit = async () => {
const anyUser = db.data.users.at(0); const anyUser = db.data.users.at(0);
@ -26,4 +28,12 @@ export const init: ServerInit = async () => {
writeFileSync("./data/default_admin_pass.txt", pass); 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);
}; };

View File

@ -6,6 +6,6 @@ export type User = {
name: string, name: string,
password: string, password: string,
admin: boolean admin: boolean
groups: Group[], groups: string[],
permissions: { [key: string]: Permission } permissions: { [key: string]: Permission }
} }

51
src/lib/server/guard.ts Normal file
View 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!;
}
}

View File

@ -1,13 +1,7 @@
import { getUserFromSession } from "$lib/server/sessions";
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types"; import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async ({ cookies }) => { export const load: LayoutServerLoad = async ({ locals: { guard } }) => {
const user = await getUserFromSession(cookies.get("session")); const user = guard.requiresAuth().orRedirects().getUser();
if (!user) {
redirect(302, "/login");
}
return { return {
user, user,

View File

@ -3,7 +3,7 @@
</script> </script>
<p>logged in as {data.user.name}</p> <p>logged in as {data.user.name}</p>
<a href="/dashboard">home</a> <a href="/dashboard/devices">devices</a>
{#if data.user.admin} {#if data.user.admin}
<a href="/dashboard/users">users</a> <a href="/dashboard/users">users</a>
<a href="/dashboard/groups">groups</a> <a href="/dashboard/groups">groups</a>

View File

@ -1,15 +1,6 @@
import { db } from "$lib/server/db/db"; import { redirect, type ServerLoad } from "@sveltejs/kit";
import type { User } from "$lib/server/db/types/user";
import type { ServerLoad } from "@sveltejs/kit";
export const load: ServerLoad = async ({ parent }) => { export const load: ServerLoad = async ({ locals: { guard }}) => {
const { user } = await parent() as { user: User }; guard.requiresAuth().orRedirects();
redirect(302, '/dashboard/devices');
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))
),
}
}; };

View File

@ -1,5 +0,0 @@
<script lang="ts">
let { data } = $props();
</script>
aaaa

View 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)
})
),
}
};

View 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}

View 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");
}
}

View 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}

View 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,
}
};

View 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}

View 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");
}
};

View 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}

View 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,
}
};

View 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}

View 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;
}
});
}
}
};

View 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}