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 # Have empty data folder ready to go
/data/* /data/*
!/data/.gitkeep !/data/.gitkeep
# Generated prisma client
generated/prisma/

View File

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

View File

@ -21,7 +21,6 @@
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prisma": "^6.5.0",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
@ -31,7 +30,6 @@
"vite": "^6.2.5" "vite": "^6.2.5"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.5.0",
"@types/wake_on_lan": "^0.0.33", "@types/wake_on_lan": "^0.0.33",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"drizzle-orm": "^0.41.0", "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 { db } from '$lib/server/db';
import type { ServerInit } from "@sveltejs/kit"; import { Guard } from '$lib/server/guard';
import bcrypt from "bcryptjs"; import { getUserFromSession } from '$lib/server/sessions';
import { nanoid } from "nanoid"; import type { ServerInit } from '@sveltejs/kit';
import { writeFileSync } from "fs"; import bcrypt from 'bcryptjs';
import { Guard } from "$lib/server/guard"; import { writeFileSync } from 'fs';
import { getUserFromSession } from "$lib/server/sessions"; import { nanoid } from 'nanoid';
export const init: ServerInit = async () => { export const init: ServerInit = async () => {
const anyUser = await prisma.user.findFirst(); const anyUser = db.data.users[0];
if (!anyUser) { if (!anyUser) {
const pass = nanoid(); const pass = nanoid();
await prisma.user.create({ await db.update(({ users }) => {
data: { users.push({
name: "admin", id: nanoid(),
password: bcrypt.hashSync(pass, 10), name: 'admin',
admin: true password: bcrypt.hashSync(pass, 10),
} admin: true,
}); groups: [],
devices: [],
});
});
console.log(`default admin password: ${pass}`); console.log(`default admin password: ${pass}`);
console.log("saved to ./default_admin_pass.txt, don't share it and change it asap"); 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 }) { 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 ...others
}: Props = $props(); }: Props = $props();
const baseClasses = "block flex items-center text-sm px-4 py-2 transition cursor-pointer rounded box-border"; let baseClasses = $state("block flex items-center text-sm px-4 py-2 cursor-pointer rounded transition-all duration-300 ease-in-out");
let colorClasses = $state("");
const defaultColors = "bg-neutral-800 hover:bg-neutral-600 text-white border border-transparent";
let colorClasses = $state(defaultColors);
let fullClasses = $derived(baseClasses + " " + extra + " " + colorClasses); let fullClasses = $derived(baseClasses + " " + extra + " " + colorClasses);
@ -35,7 +37,7 @@
colorClasses = "bg-emerald-500 hover:bg-emerald-600 text-white border border-transparent"; colorClasses = "bg-emerald-500 hover:bg-emerald-600 text-white border border-transparent";
break; break;
default: default:
colorClasses= "bg-neutral-800 hover:bg-neutral-600 text-white border border-transparent"; colorClasses= defaultColors;
} }
} else { } else {
switch (color) { 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 = { export type Device = {
id: string, id: string;
name: string, name: string;
mac: string, mac: string;
ip: string, broadcast: string;
port: number, port: number;
packets: 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 = { export type Group = {
id: string, id: string;
name: string, name: string;
permissions: { [key: string]: Permission } 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 = { export type User = {
id: string, id: string;
name: string, name: string;
password: string, password: string;
admin: boolean admin: boolean;
groups: string[], groups: string[];
permissions: { [key: string]: Permission } devices: string[];
} };

View File

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

View File

@ -1,31 +1,9 @@
import { prisma } from "$lib/server/db/db"; import { getUsersDevices } from '$lib/server/db';
import type { ServerLoad } from "@sveltejs/kit"; import type { ServerLoad } from '@sveltejs/kit';
export const load: ServerLoad = async ({ locals: { guard} }) => { export const load: ServerLoad = async ({ locals: { guard } }) => {
const user = guard.requiresAuth().orRedirects().getUser(); const user = guard.requiresAuth().orRedirects().getUser();
return {
if (user.admin) { devices: getUsersDevices(user.id),
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)),
}
};

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 { fail, redirect, type ServerLoad } from '@sveltejs/kit';
import { nanoid } from 'nanoid';
import { wake } from 'wake_on_lan'; import { wake } from 'wake_on_lan';
export const load: ServerLoad = async ({ locals: { guard }, params }) => { export const load: ServerLoad = async ({ locals: { guard }, params }) => {
guard.requiresAdmin().orRedirects(); guard.requiresAdmin().orRedirects();
const device = await prisma.device.findUnique({ const device = db.data.devices.find((d) => d.id === params.slug);
where: {
id: parseInt(params.slug!) || -1,
}
});
if (!device && params.slug !== "new") { if (!device && params.slug !== 'new') {
redirect(302, "/dashboard/devices"); redirect(302, '/dashboard/devices');
} }
return { return {
device, device,
} };
} };
export const actions = { export const actions = {
update: async ({ request, cookies, params, locals: { guard } }) => { update: async ({ request, cookies, params, locals: { guard } }) => {
if (guard.requiresAdmin().isFailed()) { if (guard.requiresAdmin().isFailed()) {
return fail(403); return fail(403);
} }
const form = await request.formData(); const form = await request.formData();
const name = form.get("name")?.toString(); const name = form.get('name')?.toString();
const mac = form.get("mac")?.toString(); const mac = form.get('mac')?.toString();
const broadcast = form.get("broadcast")?.toString(); const broadcast = form.get('broadcast')?.toString();
const port = form.get("port")?.toString(); const port = form.get('port')?.toString();
const packets = form.get("packets")?.toString(); const packets = form.get('packets')?.toString();
if (!name || !mac) { if (!name || !mac) {
// TODO better validation // TODO better validation
return { return {
error: "MISSING_FIELDS" error: 'MISSING_FIELDS',
} };
} }
try { try {
if (params.slug === "new") { if (params.slug === 'new') {
await prisma.device.create({ await db.update(({ devices }) => {
data: { devices.push({
name, id: nanoid(),
mac, name,
broadcast, mac,
port: port ? parseInt(port) : undefined, broadcast: broadcast ?? '255.255.255.255',
packets: packets ? parseInt(packets) : undefined port: port ? parseInt(port) : 9,
} packets: packets ? parseInt(packets) : 3,
}); });
} else { });
await prisma.device.update({ } else {
where: { await db.update(({ devices }) => {
id: parseInt(params.slug) let dev = devices.find((d) => d.id === 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" });
}
}
redirect(302, "/dashboard/devices"); if (!dev) {
}, return;
delete: async ({ locals: { guard }, params }) => { }
if (guard.requiresAdmin().isFailed()) {
return fail(403);
}
await prisma.device.delete({ dev.name = name;
where: { dev.mac = mac;
id: parseInt(params.slug) 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"); redirect(302, '/dashboard/devices');
}, },
wake: async ({ params, locals: { guard } }) => { delete: async ({ locals: { guard }, params }) => {
console.log("Trying to wake " + params.slug); 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()) { redirect(302, '/dashboard/devices');
console.log("Failed guard"); },
return fail(403); wake: async ({ params, locals: { guard } }) => {
} console.log('Trying to wake ' + params.slug);
const userDevices = await prisma.user.findUnique({ guard = guard.requiresAuth();
where: {
id: guard.getUser().id
},
include: {
devices: true,
groups: {
include: {
devices: true
}
}
}
});
if (!userDevices) { if (guard.isFailed()) {
console.log("Failed to find user devices"); console.log('Failed guard');
return fail(403); return fail(403);
} }
let deviceId = parseInt(params.slug); const device = getUsersDevices(guard.getUser().id).find((d) => d.id === params.slug);
if (isNaN(deviceId)) { if (!device) {
return fail(400); 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) { wake(device.mac, {
return fail(404); address: device.broadcast,
} port: device.port,
num_packets: device.packets,
console.log("Trying to wake " + device.name); });
},
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 { db } from '$lib/server/db';
import { type ServerLoad } from "@sveltejs/kit"; import { type ServerLoad } from '@sveltejs/kit';
export const load: ServerLoad = async ({ locals: { guard } }) => { export const load: ServerLoad = async ({ locals: { guard } }) => {
guard.requiresAdmin().orRedirects(); guard.requiresAdmin().orRedirects();
return {
return { groups: db.data.groups,
groups: await prisma.group.findMany({ userCounts: db.data.groups.reduce(
include: { (acc, group) => {
users: true, acc[group.id] = db.data.users.filter((u) => u.groups.includes(group.id)).length;
devices: true, return acc;
} },
}), {} as Record<string, number>,
} ),
}; };
};

View File

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Button from "$lib/components/ui/Button.svelte";
import ResourceCard from "$lib/components/resources/ResourceCard.svelte"; import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte"; import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
import IconPlus from "~icons/tabler/plus"; import Button from "$lib/components/ui/Button.svelte";
import IconEdit from "~icons/tabler/edit"; import IconEdit from "~icons/tabler/edit";
import IconPlus from "~icons/tabler/plus";
let { data } = $props(); let { data } = $props();
</script> </script>
@ -19,7 +19,7 @@
{:else} {:else}
<div class="flex gap-4 flex-wrap"> <div class="flex gap-4 flex-wrap">
{#each data.groups as group} {#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()} {#snippet actionsSnippet()}
{#if data.user.admin} {#if data.user.admin}
<Button Icon={IconEdit} a href="/dashboard/groups/{group.id}" extra="!p-2"/> <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 { db } from '$lib/server/db';
import { fail, redirect, type Actions, type ServerLoad } from "@sveltejs/kit"; import { fail, redirect, type Actions, type ServerLoad } from '@sveltejs/kit';
import { nanoid } from 'nanoid';
export const load: ServerLoad = async ({ locals: { guard },params }) => { export const load: ServerLoad = async ({ locals: { guard }, params }) => {
guard.requiresAdmin().orRedirects(); guard.requiresAdmin().orRedirects();
const group = await prisma.group.findUnique({ const group = db.data.groups.find((g) => g.id === params.slug);
where: {
id: parseInt(params.slug!) || -1,
}, include: {
devices: true
}
});
if (!group && params.slug !== "new") { if (!group && params.slug !== 'new') {
redirect(302, "/dashboard/groups"); redirect(302, '/dashboard/groups');
} }
return { return {
devices: await prisma.device.findMany(), devices: db.data.devices,
group, group,
} };
}; };
export const actions: Actions = { export const actions: Actions = {
update: async ({ request, locals: { guard }, params }) => { update: async ({ request, locals: { guard }, params }) => {
if (guard.requiresAdmin().isFailed()) { if (guard.requiresAdmin().isFailed()) {
return fail(403); return fail(403);
} }
const form = await request.formData(); const form = await request.formData();
const name = form.get("name")?.toString(); const name = form.get('name')?.toString();
const devices = form.getAll("devices").map(d => ({ id: parseInt(d.toString()) })); const devices = form.getAll('devices').map((d) => d.toString());
if (!name) { if (!name) {
// TODO better validation // TODO better validation
return { return {
error: "MISSING_FIELDS" error: 'MISSING_FIELDS',
} };
} }
if (params.slug === "new") { if (params.slug === 'new') {
await prisma.group.create({ await db.update(({ groups }) => {
data: { groups.push({
name, id: nanoid(),
devices: { name,
connect: devices devices,
} });
} });
}); } else {
} else { await db.update(({ groups }) => {
await prisma.group.update({ let group = groups.find((g) => g.id === params.slug);
where: {
id: parseInt(params.slug!) || -1,
},
data: {
name,
devices: {
set: devices
}
}
});
}
redirect(302, "/dashboard/groups"); if (!group) {
}, return;
}
delete: async ({ locals: { guard }, params }) => { group.name = name;
if (guard.requiresAdmin().isFailed()) { group.devices = devices;
return fail(403); });
} }
await prisma.group.delete({ redirect(302, '/dashboard/groups');
where: { },
id: parseInt(params.slug!) || -1,
}
});
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"> <script lang="ts">
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import Button from "$lib/components/ui/Button.svelte";
import InputSelect from "$lib/components/forms/InputSelect.svelte"; import InputSelect from "$lib/components/forms/InputSelect.svelte";
import InputText from "$lib/components/forms/InputText.svelte"; import InputText from "$lib/components/forms/InputText.svelte";
import ResourceEditPage from "$lib/components/resources/ResourceEditPage.svelte"; import ResourceEditPage from "$lib/components/resources/ResourceEditPage.svelte";
import IconDeviceFloppy from "~icons/tabler/device-floppy"; import Button from "$lib/components/ui/Button.svelte";
import IconTrash from "~icons/tabler/trash"; import IconDeviceFloppy from "~icons/tabler/device-floppy";
import IconTrash from "~icons/tabler/trash";
let { form, data } = $props(); let { form, data } = $props();
</script> </script>
@ -25,7 +25,7 @@
data={data.devices.map(d => ({ data={data.devices.map(d => ({
value: d.id.toString(), value: d.id.toString(),
name: d.name, 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"> <div class="flex gap-5 items-center">
<Button Icon={IconDeviceFloppy}>{data.group ? "Update" : "Create"}</Button> <Button Icon={IconDeviceFloppy}>{data.group ? "Update" : "Create"}</Button>

View File

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

View File

@ -1,88 +1,91 @@
import { prisma } from "$lib/server/db/db"; import { db } from '$lib/server/db';
import { fail, redirect, type Actions } from "@sveltejs/kit"; import type { User } from '$lib/server/db/types/user.js';
import bcrypt from "bcryptjs"; import { fail, redirect, type Actions } from '@sveltejs/kit';
import bcrypt from 'bcryptjs';
import { nanoid } from 'nanoid';
export const load = async ({ locals: { guard }, params }) => { export const load = async ({ locals: { guard }, params }) => {
guard.requiresAdmin().orRedirects(); guard.requiresAdmin().orRedirects();
const user = await prisma.user.findUnique({ let user = db.data.users.find((u) => u.id === params.slug) as Partial<User>;
where: {
id: parseInt(params.slug!) || -1,
},
include: {
groups: true,
devices: true
}
});
if (!user && params.slug !== "new") { if (!user && params.slug !== 'new') {
redirect(302, "/dashboard/users"); redirect(302, '/dashboard/users');
} }
return { if (user) {
user, user = structuredClone(user);
groups: await prisma.group.findMany(), delete user['password'];
devices: await prisma.device.findMany(), }
}
return {
user,
groups: db.data.groups,
devices: db.data.devices,
};
}; };
export const actions: Actions = { export const actions: Actions = {
update: async ({ request, locals: { guard }, params }) => { update: async ({ request, locals: { guard }, params }) => {
if (guard.requiresAdmin().isFailed()) { if (guard.requiresAdmin().isFailed()) {
return fail(403); return fail(403);
} }
const form = await request.formData(); const form = await request.formData();
const name = form.get("name")?.toString(); const name = form.get('name')?.toString();
const admin = form.get("admin")?.toString() === "on"; const admin = form.get('admin')?.toString() === 'on';
const password = form.get("password")?.toString() ?? ""; const password = form.get('password')?.toString() ?? '';
const groups = form.getAll("groups").map(g => ({ id: parseInt(g.toString()) })); const groups = form.getAll('groups').map((g) => g.toString());
const devices = form.getAll("devices").map(d => ({ id: parseInt(d.toString()) })); const devices = form.getAll('devices').map((d) => d.toString());
if (!name) { if (!name) {
// TODO better validation // TODO better validation
return { return {
error: "MISSING_FIELDS" error: 'MISSING_FIELDS',
} };
} }
if (params.slug === "new") { if (params.slug === 'new') {
if (password.length < 4) { if (password.length < 4) {
return { return {
error: "PASSWORD_TOO_WEAK" error: 'PASSWORD_TOO_WEAK',
} };
} }
await prisma.user.create({ await db.update(({ users }) => {
data: { users.push({
name, id: nanoid(),
password: bcrypt.hashSync(password, 10), name,
admin, admin,
groups: { groups,
connect: groups devices,
}, password: bcrypt.hashSync(password, 10),
devices: { });
connect: devices });
} } else {
} await db.update(({ users }) => {
}); let user = users.find((u) => u.id === params.slug);
} else {
await prisma.user.update({ if (!user) {
where: { return;
id: parseInt(params.slug!) || -1, }
},
data: { user.name = name;
name, user.admin = admin;
admin, user.groups = groups;
groups: { user.devices = devices;
set: groups if (password.length > 0) {
}, user.password = bcrypt.hashSync(password, 10);
devices: { }
set: devices });
}, }
password: password.length > 0 ? bcrypt.hashSync(password, 10) : undefined },
} 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"> <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 { 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(); let { data, form } = $props();
</script> </script>
@ -39,7 +39,7 @@
data={data.groups.map(g => ({ data={data.groups.map(g => ({
value: g.id.toString(), value: g.id.toString(),
name: g.name, 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 <InputSelect
label="Devices" label="Devices"
@ -48,7 +48,7 @@
data={data.devices.map(d => ({ data={data.devices.map(d => ({
value: d.id.toString(), value: d.id.toString(),
name: d.name, 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"> <div class="flex gap-5 items-center">
<Button Icon={IconDeviceFloppy}>{data.user ? "Update" : "Create"}</Button> <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 { createSession, getUserFromSession } from '$lib/server/sessions';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { prisma } from '$lib/server/db/db';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import type { Actions } from './$types';
export const actions = { export const actions = {
default: async ({ cookies, request }) => { default: async ({ cookies, request }) => {
if (await getUserFromSession(cookies.get('session'))) { if (await getUserFromSession(cookies.get('session'))) {
redirect(302, "/dashboard"); redirect(302, '/dashboard');
} }
const data = await request.formData(); const data = await request.formData();
const username = data.get("username")?.toString(); const username = data.get('username')?.toString();
const password = data.get("password")?.toString(); const password = data.get('password')?.toString();
if (!username || !password) { if (!username || !password) {
return { return {
error: "MISSING_CREDENTIALS" error: 'MISSING_CREDENTIALS',
} };
} }
const user = await prisma.user.findUnique({ const user = db.data.users.find((u) => u.name === username);
where: {
name: username
}
});
if (!user || !bcrypt.compareSync(password, user.password)) { if (!user || !bcrypt.compareSync(password, user.password)) {
return { return {
error: "INVALID_CREDENTIALS" error: 'INVALID_CREDENTIALS',
} };
} }
cookies.set("session", createSession({ cookies.set(
userAgent: request.headers.get("user-agent") ?? "UNKNOWN", 'session',
userId: user.id createSession({
}), { userAgent: request.headers.get('user-agent') ?? 'UNKNOWN',
path: "/", userId: user.id,
httpOnly: true, }),
secure: true, {
sameSite: true, path: '/',
maxAge: 60 * 60 * 24, httpOnly: true,
}); secure: true,
sameSite: true,
maxAge: 60 * 60 * 24,
},
);
redirect(302, "/dashboard"); redirect(302, '/dashboard');
} },
} satisfies Actions; } satisfies Actions;