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

3
.gitignore vendored
View File

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

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();
await prisma.user.create({
data: {
name: "admin",
await db.update(({ users }) => {
users.push({
id: nanoid(),
name: 'admin',
password: bcrypt.hashSync(pass, 10),
admin: true
}
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");
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")));
locals.guard = new Guard(await getUserFromSession(cookies.get('session')));
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,10 +1,9 @@
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();
@ -14,7 +13,7 @@ export function createSession(data: SessionData) {
sessions.set(token, data);
setTimeout(() => sessions.delete(token), 1000 * 60 * 60 * 24);
return token;
};
}
export async function getUserFromSession(sessionId?: string) {
if (!sessionId) {
@ -29,14 +28,10 @@ export async function getUserFromSession(sessionId?: string) {
// 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;
const user = await db.data.users.find((u) => u.id === data.userId);
return user;
};
}
export function deleteSession(sessionId?: string) {
if (!sessionId) return;

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)),
}
devices: getUsersDevices(user.id),
};
};

View File

@ -1,24 +1,21 @@
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();
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,
}
}
};
};
export const actions = {
update: async ({ request, cookies, params, locals: { guard } }) => {
@ -27,115 +24,92 @@ export const actions = {
}
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 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"
}
error: 'MISSING_FIELDS',
};
}
try {
if (params.slug === "new") {
await prisma.device.create({
data: {
if (params.slug === 'new') {
await db.update(({ devices }) => {
devices.push({
id: nanoid(),
name,
mac,
broadcast,
port: port ? parseInt(port) : undefined,
packets: packets ? parseInt(packets) : undefined
}
broadcast: broadcast ?? '255.255.255.255',
port: port ? parseInt(port) : 9,
packets: packets ? parseInt(packets) : 3,
});
});
} else {
await prisma.device.update({
where: {
id: parseInt(params.slug)
},
data: {
name,
mac,
broadcast,
port: port ? parseInt(port) : undefined,
packets: packets ? parseInt(packets) : undefined
await db.update(({ devices }) => {
let dev = devices.find((d) => d.id === params.slug);
if (!dev) {
return;
}
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." });
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" });
return fail(500, { error: 'DATABASE_ERROR' });
}
}
redirect(302, "/dashboard/devices");
redirect(302, '/dashboard/devices');
},
delete: async ({ locals: { guard }, params }) => {
if (guard.requiresAdmin().isFailed()) {
return fail(403);
}
await prisma.device.delete({
where: {
id: parseInt(params.slug)
}
});
db.data.devices = db.data.devices.filter((d) => d.id !== params.slug);
db.write();
redirect(302, "/dashboard/devices");
redirect(302, '/dashboard/devices');
},
wake: async ({ params, locals: { guard } }) => {
console.log("Trying to wake " + params.slug);
console.log('Trying to wake ' + params.slug);
guard = guard.requiresAuth();
if (guard.isFailed()) {
console.log("Failed guard");
console.log('Failed guard');
return fail(403);
}
const userDevices = await prisma.user.findUnique({
where: {
id: guard.getUser().id
},
include: {
devices: true,
groups: {
include: {
devices: true
}
}
}
});
if (!userDevices) {
console.log("Failed to find user devices");
return fail(403);
}
let deviceId = parseInt(params.slug);
if (isNaN(deviceId)) {
return fail(400);
}
const device = userDevices.devices.find(d => d.id === deviceId) ?? userDevices.groups.flatMap(g => g.devices).find(d => d.id === deviceId);
const device = getUsersDevices(guard.getUser().id).find((d) => d.id === params.slug);
if (!device) {
return fail(404);
}
console.log("Trying to wake " + device.name);
console.log('Trying to wake ' + device.name);
wake(device.mac, {
address: device.broadcast,
port: device.port,
num_packets: device.packets
}, () => {});
}
}
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,
}
}),
}
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 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,25 +1,20 @@
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();
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(),
devices: db.data.devices,
group,
}
};
};
export const actions: Actions = {
@ -29,40 +24,38 @@ export const actions: Actions = {
}
const form = await request.formData();
const name = form.get("name")?.toString();
const devices = form.getAll("devices").map(d => ({ id: parseInt(d.toString()) }));
const name = form.get('name')?.toString();
const devices = form.getAll('devices').map((d) => d.toString());
if (!name) {
// TODO better validation
return {
error: "MISSING_FIELDS"
}
error: 'MISSING_FIELDS',
};
}
if (params.slug === "new") {
await prisma.group.create({
data: {
if (params.slug === 'new') {
await db.update(({ groups }) => {
groups.push({
id: nanoid(),
name,
devices: {
connect: devices
}
}
devices,
});
});
} else {
await prisma.group.update({
where: {
id: parseInt(params.slug!) || -1,
},
data: {
name,
devices: {
set: devices
}
await db.update(({ groups }) => {
let group = groups.find((g) => g.id === params.slug);
if (!group) {
return;
}
group.name = name;
group.devices = devices;
});
}
redirect(302, "/dashboard/groups");
redirect(302, '/dashboard/groups');
},
delete: async ({ locals: { guard }, params }) => {
@ -70,12 +63,9 @@ export const actions: Actions = {
return fail(403);
}
await prisma.group.delete({
where: {
id: parseInt(params.slug!) || -1,
}
});
db.data.groups = db.data.groups.filter((g) => g.id !== params.slug);
db.write();
redirect(302, "/dashboard/groups");
}
redirect(302, '/dashboard/groups');
},
};

View File

@ -1,9 +1,9 @@
<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 Button from "$lib/components/ui/Button.svelte";
import IconDeviceFloppy from "~icons/tabler/device-floppy";
import IconTrash from "~icons/tabler/trash";
@ -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();
return {
users: await prisma.user.findMany({
include: {
groups: true,
devices: true
}
users: db.data.users.map((u) => {
let safeUser = structuredClone(u) as Partial<User>;
delete safeUser['password'];
return safeUser;
}),
}
};
};

View File

@ -1,29 +1,28 @@
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();
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');
}
if (user) {
user = structuredClone(user);
delete user['password'];
}
return {
user,
groups: await prisma.group.findMany(),
devices: await prisma.device.findMany(),
}
groups: db.data.groups,
devices: db.data.devices,
};
};
export const actions: Actions = {
@ -33,56 +32,60 @@ export const actions: Actions = {
}
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 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"
}
error: 'MISSING_FIELDS',
};
}
if (params.slug === "new") {
if (params.slug === 'new') {
if (password.length < 4) {
return {
error: "PASSWORD_TOO_WEAK"
}
error: 'PASSWORD_TOO_WEAK',
};
}
await prisma.user.create({
data: {
await db.update(({ users }) => {
users.push({
id: nanoid(),
name,
password: bcrypt.hashSync(password, 10),
admin,
groups: {
connect: groups
},
devices: {
connect: devices
}
}
groups,
devices,
password: bcrypt.hashSync(password, 10),
});
});
} 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 }) => {
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 { 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";
import InputToggle from "$lib/components/forms/InputToggle.svelte";
import { enhance } from "$app/forms";
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 username = data.get('username')?.toString();
const password = data.get('password')?.toString();
if (!username || !password) {
return {
error: "MISSING_CREDENTIALS"
}
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"
}
error: 'INVALID_CREDENTIALS',
};
}
cookies.set("session", createSession({
userAgent: request.headers.get("user-agent") ?? "UNKNOWN",
userId: user.id
}), {
path: "/",
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");
}
redirect(302, '/dashboard');
},
} satisfies Actions;