went back to lowdb

This commit is contained in:
axel 2025-04-11 21:06:52 +02:00
parent c419d57754
commit 54e988bd0a
27 changed files with 529 additions and 611 deletions

5
.gitignore vendored
View File

@ -24,7 +24,4 @@ vite.config.ts.timestamp-*
# Have empty data folder ready to go
/data/*
!/data/.gitkeep
# Generated prisma client
generated/prisma/
!/data/.gitkeep

View File

@ -1,7 +1,7 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"trailingComma": "all",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [

68
db.json Normal file
View File

@ -0,0 +1,68 @@
{
"users": [
{
"id": "EqWthpxM6dwLpdg6_GMNv",
"name": "admin",
"password": "$2b$10$M5f38oxNH2TbtJrSeR5bi.qgz9n5rhnxrQPh3Y.8GIZaH1Z6MgZDa",
"admin": true,
"groups": [],
"devices": [
"2PAUDI9gbzwuD8ptcptDM",
"A6u-0SaX5q5s_DzMFf3N1"
]
},
{
"id": "l6GVetq1ifjsHmnoy3TrC",
"name": "axel",
"admin": false,
"groups": [
"2Y1OFiRWu8r50_WBLaB-j"
],
"devices": [
"ps8lzN3c2uHcGtqHdmaEz"
],
"password": "$2b$10$MGb8xG3a4oxIvFq6fiqG8.X8HKS1j8WdjYjc6fgO6W.5s0vvQi8A."
}
],
"groups": [
{
"id": "2Y1OFiRWu8r50_WBLaB-j",
"name": "Gr1",
"devices": [
"2PAUDI9gbzwuD8ptcptDM",
"ps8lzN3c2uHcGtqHdmaEz"
]
},
{
"id": "gNaTU4Tdj-6-a8wRyWf21",
"name": "Gr2",
"devices": []
}
],
"devices": [
{
"id": "2PAUDI9gbzwuD8ptcptDM",
"name": "Dev1",
"mac": "00:00:00:00:00:00",
"broadcast": "255.255.255.254",
"port": 9,
"packets": 3
},
{
"id": "ps8lzN3c2uHcGtqHdmaEz",
"name": "Dev2",
"mac": "00:00:00:00:00:00",
"broadcast": "255.255.255.255",
"port": 9,
"packets": 3
},
{
"id": "A6u-0SaX5q5s_DzMFf3N1",
"name": "Dev3",
"mac": "00:00:00:00:00:00",
"broadcast": "255.255.255.255",
"port": 9,
"packets": 3
}
]
}

33
package-lock.json generated
View File

@ -8,7 +8,6 @@
"name": "my-app",
"version": "0.0.1",
"dependencies": {
"@prisma/client": "^6.5.0",
"@types/wake_on_lan": "^0.0.33",
"bcryptjs": "^3.0.2",
"drizzle-orm": "^0.41.0",
@ -24,7 +23,6 @@
"@tailwindcss/vite": "^4.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prisma": "^6.5.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
@ -774,6 +772,8 @@
"integrity": "sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=18.18"
},
@ -794,8 +794,9 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.5.0.tgz",
"integrity": "sha512-sOH/2Go9Zer67DNFLZk6pYOHj+rumSb0VILgltkoxOjYnlLqUpHPAN826vnx8HigqnOCxj9LRhT6U7uLiIIWgw==",
"devOptional": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"esbuild": ">=0.12 <1",
"esbuild-register": "3.6.0"
@ -805,16 +806,18 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.5.0.tgz",
"integrity": "sha512-fc/nusYBlJMzDmDepdUtH9aBsJrda2JNErP9AzuHbgUEQY0/9zQYZdNlXmKoIWENtio+qarPNe/+DQtrX5kMcQ==",
"devOptional": true,
"license": "Apache-2.0"
"license": "Apache-2.0",
"optional": true,
"peer": true
},
"node_modules/@prisma/engines": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.5.0.tgz",
"integrity": "sha512-FVPQYHgOllJklN9DUyujXvh3hFJCY0NX86sDmBErLvoZjy2OXGiZ5FNf3J/C4/RZZmCypZBYpBKEhx7b7rEsdw==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/debug": "6.5.0",
"@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60",
@ -826,15 +829,17 @@
"version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60.tgz",
"integrity": "sha512-iK3EmiVGFDCmXjSpdsKGNqy9hOdLnvYBrJB61far/oP03hlIxrb04OWmDjNTwtmZ3UZdA5MCvI+f+3k2jPTflQ==",
"devOptional": true,
"license": "Apache-2.0"
"license": "Apache-2.0",
"optional": true,
"peer": true
},
"node_modules/@prisma/fetch-engine": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.5.0.tgz",
"integrity": "sha512-3LhYA+FXP6pqY8FLHCjewyE8pGXXJ7BxZw2rhPq+CZAhvflVzq4K8Qly3OrmOkn6wGlz79nyLQdknyCG2HBTuA==",
"devOptional": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/debug": "6.5.0",
"@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60",
@ -845,8 +850,9 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.5.0.tgz",
"integrity": "sha512-xYcvyJwNMg2eDptBYFqFLUCfgi+wZLcj6HDMsj0Qw0irvauG4IKmkbywnqwok0B+k+W+p+jThM2DKTSmoPCkzw==",
"devOptional": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/debug": "6.5.0"
}
@ -1841,8 +1847,9 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"debug": "^4.3.4"
},
@ -1932,7 +1939,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@ -2638,9 +2644,10 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.5.0.tgz",
"integrity": "sha512-yUGXmWqv5F4PByMSNbYFxke/WbnyTLjnJ5bKr8fLkcnY7U5rU9rUTh/+Fja+gOrRxEgtCbCtca94IeITj4j/pg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/config": "6.5.0",
"@prisma/engines": "6.5.0"

View File

@ -21,7 +21,6 @@
"@tailwindcss/vite": "^4.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prisma": "^6.5.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
@ -31,7 +30,6 @@
"vite": "^6.2.5"
},
"dependencies": {
"@prisma/client": "^6.5.0",
"@types/wake_on_lan": "^0.0.33",
"bcryptjs": "^3.0.2",
"drizzle-orm": "^0.41.0",

View File

@ -1,77 +0,0 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL,
"admin" BOOLEAN NOT NULL DEFAULT false
);
-- CreateTable
CREATE TABLE "Group" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "Device" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"mac" TEXT NOT NULL,
"broadcast" TEXT NOT NULL DEFAULT '255.255.255.255',
"port" INTEGER NOT NULL DEFAULT 9,
"packets" INTEGER NOT NULL DEFAULT 3
);
-- CreateTable
CREATE TABLE "_GroupToUser" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_GroupToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Group" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_GroupToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "_DeviceToUser" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_DeviceToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Device" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_DeviceToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "_DeviceToGroup" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_DeviceToGroup_A_fkey" FOREIGN KEY ("A") REFERENCES "Device" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_DeviceToGroup_B_fkey" FOREIGN KEY ("B") REFERENCES "Group" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Group_name_key" ON "Group"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Device_name_key" ON "Device"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Device_mac_key" ON "Device"("mac");
-- CreateIndex
CREATE UNIQUE INDEX "_GroupToUser_AB_unique" ON "_GroupToUser"("A", "B");
-- CreateIndex
CREATE INDEX "_GroupToUser_B_index" ON "_GroupToUser"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_DeviceToUser_AB_unique" ON "_DeviceToUser"("A", "B");
-- CreateIndex
CREATE INDEX "_DeviceToUser_B_index" ON "_DeviceToUser"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_DeviceToGroup_AB_unique" ON "_DeviceToGroup"("A", "B");
-- CreateIndex
CREATE INDEX "_DeviceToGroup_B_index" ON "_DeviceToGroup"("B");

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

View File

@ -1,38 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:../data/db.sqlite"
}
model User {
id Int @id @default(autoincrement())
name String @unique
password String
admin Boolean @default(false)
groups Group[]
devices Device[]
}
model Group {
id Int @id @default(autoincrement())
name String @unique
users User[]
devices Device[]
}
model Device {
id Int @id @default(autoincrement())
name String @unique
mac String @unique
broadcast String @default("255.255.255.255")
port Int @default(9)
packets Int @default(3)
users User[]
groups Group[]
}

View File

@ -1,36 +1,39 @@
import { prisma } from "$lib/server/db/db";
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";
import { db } from '$lib/server/db';
import { Guard } from '$lib/server/guard';
import { getUserFromSession } from '$lib/server/sessions';
import type { ServerInit } from '@sveltejs/kit';
import bcrypt from 'bcryptjs';
import { writeFileSync } from 'fs';
import { nanoid } from 'nanoid';
export const init: ServerInit = async () => {
const anyUser = await prisma.user.findFirst();
const anyUser = db.data.users[0];
if (!anyUser) {
const pass = nanoid();
if (!anyUser) {
const pass = nanoid();
await prisma.user.create({
data: {
name: "admin",
password: bcrypt.hashSync(pass, 10),
admin: true
}
});
await db.update(({ users }) => {
users.push({
id: nanoid(),
name: 'admin',
password: bcrypt.hashSync(pass, 10),
admin: true,
groups: [],
devices: [],
});
});
console.log(`default admin password: ${pass}`);
console.log("saved to ./default_admin_pass.txt, don't share it and change it asap");
console.log(`default admin password: ${pass}`);
console.log("saved to ./default_admin_pass.txt, don't share it and change it asap");
writeFileSync("./data/default_admin_pass.txt", pass);
}
writeFileSync('./data/default_admin_pass.txt', pass);
}
};
export async function handle({ event, resolve }) {
const { cookies, locals } = event;
const { cookies, locals } = event;
locals.guard = new Guard(await getUserFromSession(cookies.get("session")));
locals.guard = new Guard(await getUserFromSession(cookies.get('session')));
return await resolve(event);
};
return await resolve(event);
}

View File

@ -20,8 +20,10 @@
...others
}: Props = $props();
const baseClasses = "block flex items-center text-sm px-4 py-2 transition cursor-pointer rounded box-border";
let colorClasses = $state("");
let baseClasses = $state("block flex items-center text-sm px-4 py-2 cursor-pointer rounded transition-all duration-300 ease-in-out");
const defaultColors = "bg-neutral-800 hover:bg-neutral-600 text-white border border-transparent";
let colorClasses = $state(defaultColors);
let fullClasses = $derived(baseClasses + " " + extra + " " + colorClasses);
@ -35,7 +37,7 @@
colorClasses = "bg-emerald-500 hover:bg-emerald-600 text-white border border-transparent";
break;
default:
colorClasses= "bg-neutral-800 hover:bg-neutral-600 text-white border border-transparent";
colorClasses= defaultColors;
}
} else {
switch (color) {

View File

@ -1,21 +0,0 @@
import { JSONFilePreset } from "lowdb/node";
import type { User } from "./types/user";
import type { Device } from "./types/device";
import type { Group } from "./types/group";
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
type Data = {
users: User[],
groups: Group[],
devices: Device[],
};
const defaultData: Data = {
users: [],
groups: [],
devices: [],
};
//export const db = await JSONFilePreset<Data>("./data/db.json", defaultData);

View File

@ -0,0 +1,42 @@
import { JSONFilePreset } from 'lowdb/node';
import type { Group } from './types/group';
import type { User } from './types/user';
import type { Device } from './types/device';
type Data = {
users: User[];
groups: Group[];
devices: Device[];
};
export const db = await JSONFilePreset<Data>('db.json', {
users: [],
groups: [],
devices: [],
});
export function getUsersDevices(userId: string): Device[] {
const user = db.data.users.find((u) => u.id === userId)!;
if (user.admin) return db.data.devices;
const userDevices = db.data.devices.filter((d) => user.devices.includes(d.id));
const userGroups = db.data.groups.filter((g) => user.groups.includes(g.id));
const groupDevices = userGroups.flatMap((g) =>
db.data.devices.filter((d) => g.devices.includes(d.id)),
);
// concat and dedupe
return [...new Set([...userDevices, ...groupDevices])];
}
/** @deprecated */
export function assignDefaults<T>(defaults: Partial<T>, obj: any, options = { mutate: false }): T {
for (let k of Object.keys(obj)) {
if (obj[k] === undefined) {
delete obj[k];
}
}
return options.mutate ? Object.assign(defaults, obj) : Object.assign({}, defaults, obj);
}

View File

@ -1,8 +1,14 @@
export type Device = {
id: string,
name: string,
mac: string,
ip: string,
port: number,
packets: number
}
id: string;
name: string;
mac: string;
broadcast: string;
port: number;
packets: number;
};
export const defaultDevice: Partial<Device> = {
broadcast: '255.255.255.255',
port: 9,
packets: 3,
};

View File

@ -1,7 +1,5 @@
import type { Permission } from "./permission"
export type Group = {
id: string,
name: string,
permissions: { [key: string]: Permission }
}
id: string;
name: string;
devices: string[];
};

View File

@ -1,3 +0,0 @@
export type Permission = {
wake: boolean;
}

View File

@ -1,11 +1,8 @@
import type { Group } from "./group"
import type { Permission } from "./permission"
export type User = {
id: string,
name: string,
password: string,
admin: boolean
groups: string[],
permissions: { [key: string]: Permission }
}
id: string;
name: string;
password: string;
admin: boolean;
groups: string[];
devices: string[];
};

View File

@ -1,44 +1,39 @@
import { nanoid } from "nanoid";
import { prisma } from "./db/db";
import type { User } from "./db/types/user";
import { nanoid } from 'nanoid';
import { db } from './db';
type SessionData = {
userId: number,
userAgent: string,
userId: string;
userAgent: string;
};
const sessions: Map<string, SessionData> = new Map();
export function createSession(data: SessionData) {
const token = nanoid();
sessions.set(token, data);
setTimeout(() => sessions.delete(token), 1000 * 60 * 60 * 24);
return token;
};
const token = nanoid();
sessions.set(token, data);
setTimeout(() => sessions.delete(token), 1000 * 60 * 60 * 24);
return token;
}
export async function getUserFromSession(sessionId?: string) {
if (!sessionId) {
return undefined;
}
const data = sessions.get(sessionId);
if (!sessionId) {
return undefined;
}
if (!data) {
return undefined;
}
const data = sessions.get(sessionId);
// what in the nested fuck is this shit
// I thought ORMs made it easier but they just make queries more ridiculous
const user = await prisma.user.findUnique({
where: {
id: data.userId
}
}) ?? undefined;
if (!data) {
return undefined;
}
return user;
};
// what in the nested fuck is this shit
// I thought ORMs made it easier but they just make queries more ridiculous
const user = await db.data.users.find((u) => u.id === data.userId);
return user;
}
export function deleteSession(sessionId?: string) {
if (!sessionId) return;
sessions.delete(sessionId);
}
if (!sessionId) return;
sessions.delete(sessionId);
}

View File

@ -1,31 +1,9 @@
import { prisma } from "$lib/server/db/db";
import type { ServerLoad } from "@sveltejs/kit";
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();
if (user.admin) {
return {
devices: await prisma.device.findMany(),
}
}
const userDevices = await prisma.user.findUnique({
where: {
id: user.id
},
include: {
devices: true,
groups: {
include: {
devices: true
}
}
}
});
return {
devices: userDevices == null ? [] :
userDevices.devices.concat(userDevices.groups.flatMap(group => group.devices)),
}
};
export const load: ServerLoad = async ({ locals: { guard } }) => {
const user = guard.requiresAuth().orRedirects().getUser();
return {
devices: getUsersDevices(user.id),
};
};

View File

@ -1,141 +1,115 @@
import { prisma } from '$lib/server/db/db';
import { db, getUsersDevices } from '$lib/server/db/index.js';
import { fail, redirect, type ServerLoad } from '@sveltejs/kit';
import { nanoid } from 'nanoid';
import { wake } from 'wake_on_lan';
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
guard.requiresAdmin().orRedirects();
guard.requiresAdmin().orRedirects();
const device = await prisma.device.findUnique({
where: {
id: parseInt(params.slug!) || -1,
}
});
const device = db.data.devices.find((d) => d.id === params.slug);
if (!device && params.slug !== "new") {
redirect(302, "/dashboard/devices");
}
if (!device && params.slug !== 'new') {
redirect(302, '/dashboard/devices');
}
return {
device,
}
}
return {
device,
};
};
export const actions = {
update: async ({ request, cookies, params, locals: { guard } }) => {
if (guard.requiresAdmin().isFailed()) {
return fail(403);
}
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 broadcast = form.get("broadcast")?.toString();
const port = form.get("port")?.toString();
const packets = form.get("packets")?.toString();
const form = await request.formData();
const name = form.get('name')?.toString();
const mac = form.get('mac')?.toString();
const broadcast = form.get('broadcast')?.toString();
const port = form.get('port')?.toString();
const packets = form.get('packets')?.toString();
if (!name || !mac) {
// TODO better validation
return {
error: "MISSING_FIELDS"
}
}
if (!name || !mac) {
// TODO better validation
return {
error: 'MISSING_FIELDS',
};
}
try {
if (params.slug === "new") {
await prisma.device.create({
data: {
name,
mac,
broadcast,
port: port ? parseInt(port) : undefined,
packets: packets ? parseInt(packets) : undefined
}
});
} else {
await prisma.device.update({
where: {
id: parseInt(params.slug)
},
data: {
name,
mac,
broadcast,
port: port ? parseInt(port) : undefined,
packets: packets ? parseInt(packets) : undefined
}
});
}
} catch (e: any) {
if (e.code === "P2002") {
return fail(409, { error: "This name or this MAC adress is already in use. Please make sure they are unique." });
} else {
console.error(e);
return fail(500, { error: "DATABASE_ERROR" });
}
}
try {
if (params.slug === 'new') {
await db.update(({ devices }) => {
devices.push({
id: nanoid(),
name,
mac,
broadcast: broadcast ?? '255.255.255.255',
port: port ? parseInt(port) : 9,
packets: packets ? parseInt(packets) : 3,
});
});
} else {
await db.update(({ devices }) => {
let dev = devices.find((d) => d.id === params.slug);
redirect(302, "/dashboard/devices");
},
delete: async ({ locals: { guard }, params }) => {
if (guard.requiresAdmin().isFailed()) {
return fail(403);
}
if (!dev) {
return;
}
await prisma.device.delete({
where: {
id: parseInt(params.slug)
}
});
dev.name = name;
dev.mac = mac;
dev.broadcast = broadcast ?? dev.broadcast;
dev.port = port ? parseInt(port) : dev.port;
dev.packets = packets ? parseInt(packets) : dev.packets;
});
}
} catch (e: any) {
if (e.code === 'P2002') {
return fail(409, {
error:
'This name or this MAC adress is already in use. Please make sure they are unique.',
});
} else {
console.error(e);
return fail(500, { error: 'DATABASE_ERROR' });
}
}
redirect(302, "/dashboard/devices");
},
wake: async ({ params, locals: { guard } }) => {
console.log("Trying to wake " + params.slug);
redirect(302, '/dashboard/devices');
},
delete: async ({ locals: { guard }, params }) => {
if (guard.requiresAdmin().isFailed()) {
return fail(403);
}
guard = guard.requiresAuth();
db.data.devices = db.data.devices.filter((d) => d.id !== params.slug);
db.write();
if (guard.isFailed()) {
console.log("Failed guard");
return fail(403);
}
redirect(302, '/dashboard/devices');
},
wake: async ({ params, locals: { guard } }) => {
console.log('Trying to wake ' + params.slug);
const userDevices = await prisma.user.findUnique({
where: {
id: guard.getUser().id
},
include: {
devices: true,
groups: {
include: {
devices: true
}
}
}
});
guard = guard.requiresAuth();
if (!userDevices) {
console.log("Failed to find user devices");
return fail(403);
}
if (guard.isFailed()) {
console.log('Failed guard');
return fail(403);
}
let deviceId = parseInt(params.slug);
const device = getUsersDevices(guard.getUser().id).find((d) => d.id === params.slug);
if (isNaN(deviceId)) {
return fail(400);
}
if (!device) {
return fail(404);
}
const device = userDevices.devices.find(d => d.id === deviceId) ?? userDevices.groups.flatMap(g => g.devices).find(d => d.id === deviceId);
console.log('Trying to wake ' + device.name);
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
}, () => {});
}
}
wake(device.mac, {
address: device.broadcast,
port: device.port,
num_packets: device.packets,
});
},
};

View File

@ -1,15 +1,16 @@
import { prisma } from "$lib/server/db/db";
import { type ServerLoad } from "@sveltejs/kit";
import { db } from '$lib/server/db';
import { type ServerLoad } from '@sveltejs/kit';
export const load: ServerLoad = async ({ locals: { guard } }) => {
guard.requiresAdmin().orRedirects();
return {
groups: await prisma.group.findMany({
include: {
users: true,
devices: true,
}
}),
}
};
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>,
),
};
};

View File

@ -1,9 +1,9 @@
<script lang="ts">
import Button from "$lib/components/ui/Button.svelte";
import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
import IconPlus from "~icons/tabler/plus";
import IconEdit from "~icons/tabler/edit";
import Button from "$lib/components/ui/Button.svelte";
import IconEdit from "~icons/tabler/edit";
import IconPlus from "~icons/tabler/plus";
let { data } = $props();
</script>
@ -19,7 +19,7 @@
{:else}
<div class="flex gap-4 flex-wrap">
{#each data.groups as group}
<ResourceCard title={group.name} subtitle="{group.users.length} users, {group.devices.length} devices">
<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"/>

View File

@ -1,81 +1,71 @@
import { prisma } from "$lib/server/db/db";
import { fail, redirect, type Actions, type ServerLoad } from "@sveltejs/kit";
import { db } from '$lib/server/db';
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();
export const load: ServerLoad = async ({ locals: { guard }, params }) => {
guard.requiresAdmin().orRedirects();
const group = await prisma.group.findUnique({
where: {
id: parseInt(params.slug!) || -1,
}, include: {
devices: true
}
});
const group = db.data.groups.find((g) => g.id === params.slug);
if (!group && params.slug !== "new") {
redirect(302, "/dashboard/groups");
}
if (!group && params.slug !== 'new') {
redirect(302, '/dashboard/groups');
}
return {
devices: await prisma.device.findMany(),
group,
}
return {
devices: db.data.devices,
group,
};
};
export const actions: Actions = {
update: async ({ request, locals: { guard }, params }) => {
if (guard.requiresAdmin().isFailed()) {
return fail(403);
}
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 devices = form.getAll("devices").map(d => ({ id: parseInt(d.toString()) }));
const form = await request.formData();
const name = form.get('name')?.toString();
const devices = form.getAll('devices').map((d) => d.toString());
if (!name) {
// TODO better validation
return {
error: "MISSING_FIELDS"
}
}
if (!name) {
// TODO better validation
return {
error: 'MISSING_FIELDS',
};
}
if (params.slug === "new") {
await prisma.group.create({
data: {
name,
devices: {
connect: devices
}
}
});
} else {
await prisma.group.update({
where: {
id: parseInt(params.slug!) || -1,
},
data: {
name,
devices: {
set: devices
}
}
});
}
if (params.slug === 'new') {
await db.update(({ groups }) => {
groups.push({
id: nanoid(),
name,
devices,
});
});
} else {
await db.update(({ groups }) => {
let group = groups.find((g) => g.id === params.slug);
redirect(302, "/dashboard/groups");
},
if (!group) {
return;
}
delete: async ({ locals: { guard }, params }) => {
if (guard.requiresAdmin().isFailed()) {
return fail(403);
}
group.name = name;
group.devices = devices;
});
}
await prisma.group.delete({
where: {
id: parseInt(params.slug!) || -1,
}
});
redirect(302, '/dashboard/groups');
},
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.write();
redirect(302, '/dashboard/groups');
},
};

View File

@ -1,11 +1,11 @@
<script lang="ts">
import { enhance } from "$app/forms";
import Button from "$lib/components/ui/Button.svelte";
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 IconDeviceFloppy from "~icons/tabler/device-floppy";
import IconTrash from "~icons/tabler/trash";
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>
@ -25,7 +25,7 @@
data={data.devices.map(d => ({
value: d.id.toString(),
name: d.name,
selected: data.group?.devices.find(gd => gd.id === d.id) ? true : false }))}/>
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>

View File

@ -1,15 +1,16 @@
import { prisma } from "$lib/server/db/db";
import type { ServerLoad } from "@sveltejs/kit";
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();
guard.requiresAdmin().orRedirects();
return {
users: await prisma.user.findMany({
include: {
groups: true,
devices: true
}
}),
}
};
return {
users: db.data.users.map((u) => {
let safeUser = structuredClone(u) as Partial<User>;
delete safeUser['password'];
return safeUser;
}),
};
};

View File

@ -1,88 +1,91 @@
import { prisma } from "$lib/server/db/db";
import { fail, redirect, type Actions } from "@sveltejs/kit";
import bcrypt from "bcryptjs";
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';
export const load = async ({ locals: { guard }, params }) => {
guard.requiresAdmin().orRedirects();
guard.requiresAdmin().orRedirects();
const user = await prisma.user.findUnique({
where: {
id: parseInt(params.slug!) || -1,
},
include: {
groups: true,
devices: true
}
});
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 && params.slug !== 'new') {
redirect(302, '/dashboard/users');
}
return {
user,
groups: await prisma.group.findMany(),
devices: await prisma.device.findMany(),
}
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);
}
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(g => ({ id: parseInt(g.toString()) }));
const devices = form.getAll("devices").map(d => ({ id: parseInt(d.toString()) }));
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((g) => g.toString());
const devices = form.getAll('devices').map((d) => d.toString());
if (!name) {
// TODO better validation
return {
error: "MISSING_FIELDS"
}
}
if (!name) {
// TODO better validation
return {
error: 'MISSING_FIELDS',
};
}
if (params.slug === "new") {
if (password.length < 4) {
return {
error: "PASSWORD_TOO_WEAK"
}
}
if (params.slug === 'new') {
if (password.length < 4) {
return {
error: 'PASSWORD_TOO_WEAK',
};
}
await prisma.user.create({
data: {
name,
password: bcrypt.hashSync(password, 10),
admin,
groups: {
connect: groups
},
devices: {
connect: devices
}
}
});
} else {
await prisma.user.update({
where: {
id: parseInt(params.slug!) || -1,
},
data: {
name,
admin,
groups: {
set: groups
},
devices: {
set: devices
},
password: password.length > 0 ? bcrypt.hashSync(password, 10) : undefined
}
});
}
}
};
await db.update(({ users }) => {
users.push({
id: nanoid(),
name,
admin,
groups,
devices,
password: bcrypt.hashSync(password, 10),
});
});
} else {
await db.update(({ users }) => {
let user = users.find((u) => u.id === params.slug);
if (!user) {
return;
}
user.name = name;
user.admin = admin;
user.groups = groups;
user.devices = devices;
if (password.length > 0) {
user.password = bcrypt.hashSync(password, 10);
}
});
}
},
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();
},
};

View File

@ -1,12 +1,12 @@
<script lang="ts">
import Button from "$lib/components/ui/Button.svelte";
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 IconDeviceFloppy from "~icons/tabler/device-floppy";
import IconTrash from "~icons/tabler/trash";
import InputToggle from "$lib/components/forms/InputToggle.svelte";
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>
@ -39,7 +39,7 @@
data={data.groups.map(g => ({
value: g.id.toString(),
name: g.name,
selected: data.user?.groups.find(ug => ug.id === g.id) ? true : false }))}/>
selected: data.user?.groups?.find(ug => ug === g.id) ? true : false }))}/>
<InputSelect
label="Devices"
@ -48,7 +48,7 @@
data={data.devices.map(d => ({
value: d.id.toString(),
name: d.name,
selected: data.user?.devices.find(ud => ud.id === d.id) ? true : false }))}/>
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>

View File

@ -1,48 +1,48 @@
import { db } from '$lib/server/db';
import { createSession, getUserFromSession } from '$lib/server/sessions';
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { prisma } from '$lib/server/db/db';
import bcrypt from 'bcryptjs';
import type { Actions } from './$types';
export const actions = {
default: async ({ cookies, request }) => {
if (await getUserFromSession(cookies.get('session'))) {
redirect(302, "/dashboard");
}
redirect(302, '/dashboard');
}
const data = await request.formData();
const username = data.get("username")?.toString();
const password = data.get("password")?.toString();
const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();
if (!username || !password) {
return {
error: "MISSING_CREDENTIALS"
}
}
if (!username || !password) {
return {
error: 'MISSING_CREDENTIALS',
};
}
const user = await prisma.user.findUnique({
where: {
name: username
}
});
const user = db.data.users.find((u) => u.name === username);
if (!user || !bcrypt.compareSync(password, user.password)) {
return {
error: "INVALID_CREDENTIALS"
}
}
if (!user || !bcrypt.compareSync(password, user.password)) {
return {
error: 'INVALID_CREDENTIALS',
};
}
cookies.set("session", createSession({
userAgent: request.headers.get("user-agent") ?? "UNKNOWN",
userId: user.id
}), {
path: "/",
httpOnly: true,
secure: true,
sameSite: true,
maxAge: 60 * 60 * 24,
});
cookies.set(
'session',
createSession({
userAgent: request.headers.get('user-agent') ?? 'UNKNOWN',
userId: user.id,
}),
{
path: '/',
httpOnly: true,
secure: true,
sameSite: true,
maxAge: 60 * 60 * 24,
},
);
redirect(302, "/dashboard");
}
} satisfies Actions;
redirect(302, '/dashboard');
},
} satisfies Actions;