From c419d57754631bd5c4032834720205c0e563138f Mon Sep 17 00:00:00 2001 From: axel Date: Wed, 9 Apr 2025 09:57:55 +0200 Subject: [PATCH] feat & styling --- .gitignore | 5 +- package-lock.json | 439 +++++++++++++++++- package.json | 8 +- .../20250408121716_init/migration.sql | 77 +++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 38 ++ src/app.css | 5 + src/app.d.ts | 1 + src/hooks.server.ts | 17 +- src/lib/components/forms/InputSelect.svelte | 51 ++ src/lib/components/forms/InputText.svelte | 20 + src/lib/components/forms/InputToggle.svelte | 26 ++ .../components/resources/ResourceCard.svelte | 19 + .../resources/ResourceEditPage.svelte | 38 ++ .../resources/ResourceListPage.svelte | 22 + .../resources/ResourcePageHeader.svelte | 8 + src/lib/components/ui/Button.svelte | 73 +++ src/lib/components/ui/Collapsible.svelte | 19 + src/lib/components/ui/HorizontalSpacer.svelte | 1 + src/lib/server/db/db.ts | 5 +- src/lib/server/guard.ts | 6 +- src/lib/server/sessions.ts | 12 +- src/routes/dashboard/+layout.svelte | 65 ++- src/routes/dashboard/devices/+page.server.ts | 32 +- src/routes/dashboard/devices/+page.svelte | 48 +- .../dashboard/devices/[slug]/+page.server.ts | 129 +++-- .../dashboard/devices/[slug]/+page.svelte | 78 ++-- src/routes/dashboard/groups/+page.server.ts | 9 +- src/routes/dashboard/groups/+page.svelte | 39 +- .../dashboard/groups/[slug]/+page.server.ts | 69 ++- .../dashboard/groups/[slug]/+page.svelte | 61 +-- src/routes/dashboard/users/+page.server.ts | 9 +- src/routes/dashboard/users/+page.svelte | 33 +- .../dashboard/users/[slug]/+page.server.ts | 78 ++-- .../dashboard/users/[slug]/+page.svelte | 100 ++-- src/routes/login/+page.server.ts | 8 +- vite.config.ts | 3 +- 37 files changed, 1344 insertions(+), 310 deletions(-) create mode 100644 prisma/migrations/20250408121716_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/lib/components/forms/InputSelect.svelte create mode 100644 src/lib/components/forms/InputText.svelte create mode 100644 src/lib/components/forms/InputToggle.svelte create mode 100644 src/lib/components/resources/ResourceCard.svelte create mode 100644 src/lib/components/resources/ResourceEditPage.svelte create mode 100644 src/lib/components/resources/ResourceListPage.svelte create mode 100644 src/lib/components/resources/ResourcePageHeader.svelte create mode 100644 src/lib/components/ui/Button.svelte create mode 100644 src/lib/components/ui/Collapsible.svelte create mode 100644 src/lib/components/ui/HorizontalSpacer.svelte diff --git a/.gitignore b/.gitignore index 6e0715f..56420a5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ vite.config.ts.timestamp-* # Have empty data folder ready to go /data/* -!/data/.gitkeep \ No newline at end of file +!/data/.gitkeep + +# Generated prisma client +generated/prisma/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a18b1ee..6e5c80a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,23 +8,29 @@ "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", "lowdb": "^7.0.1", - "nanoid": "^5.1.5" + "nanoid": "^5.1.5", + "wake_on_lan": "^1.0.0" }, "devDependencies": { + "@iconify-json/tabler": "^1.2.17", "@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@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", "tsx": "^4.19.3", "typescript": "^5.0.0", + "unplugin-icons": "^22.1.0", "vite": "^6.2.5" } }, @@ -42,6 +48,30 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz", + "integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^0.2.8", + "tinyexec": "^0.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -467,6 +497,40 @@ "node": ">=18" } }, + "node_modules/@iconify-json/tabler": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@iconify-json/tabler/-/tabler-1.2.17.tgz", + "integrity": "sha512-Jfk20IC/n7UOQQSXM600BUhAwEfg8KU1dNUF+kg4eRhbET5w1Ktyax7CDx8Z8y0H6+J/8//AXpJOEgG8YoP8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -704,6 +768,89 @@ "dev": true, "license": "MIT" }, + "node_modules/@prisma/client": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.5.0.tgz", + "integrity": "sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "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", + "dependencies": { + "esbuild": ">=0.12 <1", + "esbuild-register": "3.6.0" + } + }, + "node_modules/@prisma/debug": { + "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" + }, + "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", + "dependencies": { + "@prisma/debug": "6.5.0", + "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "@prisma/fetch-engine": "6.5.0", + "@prisma/get-platform": "6.5.0" + } + }, + "node_modules/@prisma/engines-version": { + "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" + }, + "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", + "dependencies": { + "@prisma/debug": "6.5.0", + "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "@prisma/get-platform": "6.5.0" + } + }, + "node_modules/@prisma/get-platform": { + "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", + "dependencies": { + "@prisma/debug": "6.5.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.39.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz", @@ -1335,12 +1482,19 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/wake_on_lan": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/wake_on_lan/-/wake_on_lan-0.0.33.tgz", + "integrity": "sha512-kUDV5jTVq9iAeWfZfBhWFnLUXl5ggbCpJEYiXwMmisuQaHbfpZuo2DbOPHio9ZBMcqC45vuBAn/YKr1QRqelog==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1420,6 +1574,13 @@ "node": ">=6" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -1639,7 +1800,7 @@ "version": "0.25.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1676,6 +1837,19 @@ "@esbuild/win32-x64": "0.25.2" } }, + "node_modules/esbuild-register": { + "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", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -1693,6 +1867,13 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/exsolve": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.4.tgz", + "integrity": "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", @@ -1797,6 +1978,19 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1864,6 +2058,13 @@ "node": ">=6" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, "node_modules/libsql": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.4.tgz", @@ -2145,6 +2346,24 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -2177,6 +2396,47 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2263,6 +2523,23 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2270,6 +2547,18 @@ "dev": true, "license": "ISC" }, + "node_modules/pkg-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", + "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.1", + "exsolve": "^1.0.1", + "pathe": "^2.0.3" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -2345,6 +2634,35 @@ "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, + "node_modules/prisma": { + "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", + "dependencies": { + "@prisma/config": "6.5.0", + "@prisma/engines": "6.5.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/promise-limit": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", @@ -2353,6 +2671,23 @@ "optional": true, "peer": true }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2569,6 +2904,13 @@ "node": ">=6" } }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -2603,7 +2945,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2613,13 +2955,77 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.2.2.tgz", + "integrity": "sha512-Qp+iiD+qCRnUek+nDoYvtWX7tfnYyXsrOnJ452FRTgOyKmTM7TUJ3l+PLPJOOWPTUyKISKp4isC5JJPSXUjGgw==", + "dev": true, "license": "MIT", - "optional": true, - "peer": true + "dependencies": { + "acorn": "^8.14.1", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-icons": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-22.1.0.tgz", + "integrity": "sha512-ect2ZNtk1Zgwb0NVHd0C1IDW/MV+Jk/xaq4t8o6rYdVS3+L660ZdD5kTSQZvsgdwCvquRw+/wYn75hsweRjoIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@iconify/utils": "^2.3.0", + "debug": "^4.4.0", + "local-pkg": "^1.0.0", + "unplugin": "^2.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@svgr/core": ">=7.0.0", + "@svgx/core": "^1.0.1", + "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0", + "vue-template-compiler": "^2.6.12", + "vue-template-es2015-compiler": "^1.9.0" + }, + "peerDependenciesMeta": { + "@svgr/core": { + "optional": true + }, + "@svgx/core": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + }, + "vue-template-es2015-compiler": { + "optional": true + } + } }, "node_modules/vite": { "version": "6.2.5", @@ -2712,6 +3118,18 @@ } } }, + "node_modules/wake_on_lan": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wake_on_lan/-/wake_on_lan-1.0.0.tgz", + "integrity": "sha512-0QSpxny0QmsssshI6kePj6cobQPK+i8r5shfj58ZfQIUH9fUTyAaYPqZO3W/Ai7mN4vQVdTdsSGIr20M81UL6Q==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "wake": "wake" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -2723,6 +3141,13 @@ "node": ">= 8" } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", diff --git a/package.json b/package.json index 85a18d7..f8d827d 100644 --- a/package.json +++ b/package.json @@ -14,24 +14,30 @@ "lint": "prettier --check ." }, "devDependencies": { + "@iconify-json/tabler": "^1.2.17", "@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@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", "tsx": "^4.19.3", "typescript": "^5.0.0", + "unplugin-icons": "^22.1.0", "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", "lowdb": "^7.0.1", - "nanoid": "^5.1.5" + "nanoid": "^5.1.5", + "wake_on_lan": "^1.0.0" }, "overrides": { "@sveltejs/kit": { diff --git a/prisma/migrations/20250408121716_init/migration.sql b/prisma/migrations/20250408121716_init/migration.sql new file mode 100644 index 0000000..2b6a6c9 --- /dev/null +++ b/prisma/migrations/20250408121716_init/migration.sql @@ -0,0 +1,77 @@ +-- 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"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e1640d1 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..1c178c6 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,38 @@ +// 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[] +} diff --git a/src/app.css b/src/app.css index d4b5078..736ecf1 100644 --- a/src/app.css +++ b/src/app.css @@ -1 +1,6 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); @import 'tailwindcss'; + +* { + font-family: "Inter", sans-serif; +} \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index 3c856c0..693aaee 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,6 +1,7 @@ // See https://svelte.dev/docs/kit/types#app.d.ts import type { Guard } from "$lib/server/guard"; +import 'unplugin-icons/types/svelte'; // for information about these interfaces declare global { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 2dd97ee..2cca2fc 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,4 +1,4 @@ -import { db } from "$lib/server/db/db"; +import { prisma } from "$lib/server/db/db"; import type { ServerInit } from "@sveltejs/kit"; import bcrypt from "bcryptjs"; import { nanoid } from "nanoid"; @@ -7,20 +7,17 @@ import { Guard } from "$lib/server/guard"; import { getUserFromSession } from "$lib/server/sessions"; export const init: ServerInit = async () => { - const anyUser = db.data.users.at(0); + const anyUser = await prisma.user.findFirst(); if (!anyUser) { - const pass = nanoid(12); + const pass = nanoid(); - await db.update(({ users }) => { - users.push({ - id: nanoid(), + await prisma.user.create({ + data: { name: "admin", password: bcrypt.hashSync(pass, 10), - admin: true, - groups: [], - permissions: {} - }); + admin: true + } }); console.log(`default admin password: ${pass}`); diff --git a/src/lib/components/forms/InputSelect.svelte b/src/lib/components/forms/InputSelect.svelte new file mode 100644 index 0000000..580296c --- /dev/null +++ b/src/lib/components/forms/InputSelect.svelte @@ -0,0 +1,51 @@ + + +
+ + + +
+ {#if selectData.length == 0} +

No data

+ {:else} + {#each selectData as el} + + {/each} + {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/forms/InputText.svelte b/src/lib/components/forms/InputText.svelte new file mode 100644 index 0000000..f262829 --- /dev/null +++ b/src/lib/components/forms/InputText.svelte @@ -0,0 +1,20 @@ + + +
+ + +
\ No newline at end of file diff --git a/src/lib/components/forms/InputToggle.svelte b/src/lib/components/forms/InputToggle.svelte new file mode 100644 index 0000000..fa6088c --- /dev/null +++ b/src/lib/components/forms/InputToggle.svelte @@ -0,0 +1,26 @@ + + +
+ + + + +
\ No newline at end of file diff --git a/src/lib/components/resources/ResourceCard.svelte b/src/lib/components/resources/ResourceCard.svelte new file mode 100644 index 0000000..cb65625 --- /dev/null +++ b/src/lib/components/resources/ResourceCard.svelte @@ -0,0 +1,19 @@ + + +
+
+

{title}

+

{subtitle}

+
+
+ {#if actionsSnippet} + {@render actionsSnippet()} + {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/resources/ResourceEditPage.svelte b/src/lib/components/resources/ResourceEditPage.svelte new file mode 100644 index 0000000..22210e3 --- /dev/null +++ b/src/lib/components/resources/ResourceEditPage.svelte @@ -0,0 +1,38 @@ + + +
+ + + + + + +
+ {@render children()} +
+ + {#if error} +
+ +
+ {/if} +
\ No newline at end of file diff --git a/src/lib/components/resources/ResourceListPage.svelte b/src/lib/components/resources/ResourceListPage.svelte new file mode 100644 index 0000000..1497fd3 --- /dev/null +++ b/src/lib/components/resources/ResourceListPage.svelte @@ -0,0 +1,22 @@ + + +
+ + {@render actionSnippet()} + + + + +
+ {@render contentSnippet()} +
+
\ No newline at end of file diff --git a/src/lib/components/resources/ResourcePageHeader.svelte b/src/lib/components/resources/ResourcePageHeader.svelte new file mode 100644 index 0000000..080f7d0 --- /dev/null +++ b/src/lib/components/resources/ResourcePageHeader.svelte @@ -0,0 +1,8 @@ + + +
+

{title}

+ {@render children()} +
\ No newline at end of file diff --git a/src/lib/components/ui/Button.svelte b/src/lib/components/ui/Button.svelte new file mode 100644 index 0000000..c8a2b85 --- /dev/null +++ b/src/lib/components/ui/Button.svelte @@ -0,0 +1,73 @@ + + +{#if a} + + {#if Icon} + + {/if} + {#if children} + {@render children()} + {/if} + +{:else} + +{/if} \ No newline at end of file diff --git a/src/lib/components/ui/Collapsible.svelte b/src/lib/components/ui/Collapsible.svelte new file mode 100644 index 0000000..74fb14e --- /dev/null +++ b/src/lib/components/ui/Collapsible.svelte @@ -0,0 +1,19 @@ + + +
+ + + {#if expanded} +
+ {@render children()} +
+ {/if} +
\ No newline at end of file diff --git a/src/lib/components/ui/HorizontalSpacer.svelte b/src/lib/components/ui/HorizontalSpacer.svelte new file mode 100644 index 0000000..69ebaea --- /dev/null +++ b/src/lib/components/ui/HorizontalSpacer.svelte @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/lib/server/db/db.ts b/src/lib/server/db/db.ts index 243ef4e..adeb689 100644 --- a/src/lib/server/db/db.ts +++ b/src/lib/server/db/db.ts @@ -2,6 +2,9 @@ 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[], @@ -15,4 +18,4 @@ const defaultData: Data = { devices: [], }; -export const db = await JSONFilePreset("./data/db.json", defaultData); \ No newline at end of file +//export const db = await JSONFilePreset("./data/db.json", defaultData); \ No newline at end of file diff --git a/src/lib/server/guard.ts b/src/lib/server/guard.ts index 44fe3ae..bcba437 100644 --- a/src/lib/server/guard.ts +++ b/src/lib/server/guard.ts @@ -1,5 +1,5 @@ -import { fail, redirect } from "@sveltejs/kit"; -import type { User } from "./db/types/user"; +import { redirect } from "@sveltejs/kit"; +import type { User } from "@prisma/client"; export class Guard { private readonly user?: User; @@ -45,7 +45,7 @@ export class Guard { return false; } - public getUser(): User { + public getUser() { return this.user!; } } \ No newline at end of file diff --git a/src/lib/server/sessions.ts b/src/lib/server/sessions.ts index c38ad55..0bdcbf1 100644 --- a/src/lib/server/sessions.ts +++ b/src/lib/server/sessions.ts @@ -1,9 +1,9 @@ import { nanoid } from "nanoid"; -import { db } from "./db/db"; +import { prisma } from "./db/db"; import type { User } from "./db/types/user"; type SessionData = { - userId: string, + userId: number, userAgent: string, }; @@ -16,7 +16,7 @@ export function createSession(data: SessionData) { return token; }; -export async function getUserFromSession(sessionId?: string): Promise { +export async function getUserFromSession(sessionId?: string) { if (!sessionId) { return undefined; } @@ -29,7 +29,11 @@ export async function getUserFromSession(sessionId?: string): Promise user.id === data.userId); + const user = await prisma.user.findUnique({ + where: { + id: data.userId + } + }) ?? undefined; return user; }; diff --git a/src/routes/dashboard/+layout.svelte b/src/routes/dashboard/+layout.svelte index adddfdb..28c5eef 100644 --- a/src/routes/dashboard/+layout.svelte +++ b/src/routes/dashboard/+layout.svelte @@ -1,15 +1,60 @@ -

logged in as {data.user.name}

-devices -{#if data.user.admin} -users -groups -{/if} -logout +
+ + + + +
+ {@render children()} +
+
+ diff --git a/src/routes/dashboard/devices/+page.server.ts b/src/routes/dashboard/devices/+page.server.ts index 477dd06..461e07c 100644 --- a/src/routes/dashboard/devices/+page.server.ts +++ b/src/routes/dashboard/devices/+page.server.ts @@ -1,17 +1,31 @@ -import { db } from "$lib/server/db/db"; +import { prisma } from "$lib/server/db/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: user.admin ? db.data.devices : - db.data.devices.filter(device => - Object.keys(user.permissions).includes(device.id) || - user.groups.some(groupId => { - const group = db.data.groups.find(group => group.id === groupId); - return group && Object.keys(group.permissions).includes(device.id) - }) - ), + devices: userDevices == null ? [] : + userDevices.devices.concat(userDevices.groups.flatMap(group => group.devices)), } }; \ No newline at end of file diff --git a/src/routes/dashboard/devices/+page.svelte b/src/routes/dashboard/devices/+page.svelte index 6637499..5a01c7a 100644 --- a/src/routes/dashboard/devices/+page.svelte +++ b/src/routes/dashboard/devices/+page.svelte @@ -1,18 +1,40 @@ -{#if data.user.admin} - new device -{/if} + + {#snippet actionSnippet()} + {#if data.user.admin} + + {/if} + {/snippet} -{#if data.devices.length === 0} -

No devices found

-{:else} -

Devices:

-
    - {#each data.devices as device} -
  • {device.name} {#if data.user.admin}- edit{/if}
  • - {/each} -
-{/if} \ No newline at end of file + {#snippet contentSnippet()} + {#if data.devices.length === 0} +

No devices found

+ {:else} +
+ {#each data.devices as device} + + {#snippet actionsSnippet()} + {#if data.user.admin} + + + {/snippet} + + {/each} +
+ {/if} + {/snippet} +
\ No newline at end of file diff --git a/src/routes/dashboard/devices/[slug]/+page.server.ts b/src/routes/dashboard/devices/[slug]/+page.server.ts index 2fdf03d..15d74ca 100644 --- a/src/routes/dashboard/devices/[slug]/+page.server.ts +++ b/src/routes/dashboard/devices/[slug]/+page.server.ts @@ -1,12 +1,15 @@ -import { db } from '$lib/server/db/db'; -import { getUserFromSession } from '$lib/server/sessions'; +import { prisma } from '$lib/server/db/db'; 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 = db.data.devices.find(device => device.id === params.slug); + const device = await prisma.device.findUnique({ + where: { + id: parseInt(params.slug!) || -1, + } + }); if (!device && params.slug !== "new") { redirect(302, "/dashboard/devices"); @@ -26,42 +29,49 @@ export const actions = { const form = await request.formData(); const name = form.get("name")?.toString(); const mac = form.get("mac")?.toString(); - const ip = form.get("ip")?.toString(); + const broadcast = form.get("broadcast")?.toString(); const port = form.get("port")?.toString(); const packets = form.get("packets")?.toString(); - if (!name || !mac || !ip || !port || !packets) { + if (!name || !mac) { // TODO better validation return { error: "MISSING_FIELDS" } } - if (params.slug === "new") { - await db.update(({ devices }) => { - devices.push({ - id: nanoid(), - name, - mac, - ip, - port: parseInt(port), - packets: parseInt(packets) + 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 + } }); - }); - } else { - let device = db.data.devices.find(device => device.id === params.slug); - - if (!device) { - return; } - - device.name = name; - device.mac = mac; - device.ip = ip; - device.port = parseInt(port); - device.packets = parseInt(packets); - - await db.write(); + } 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"); @@ -71,16 +81,61 @@ export const actions = { return fail(403); } - db.data.devices = db.data.devices.filter(device => device.id !== params.slug); - db.data.users.forEach(user => { - delete user.permissions[params.slug]; + await prisma.device.delete({ + where: { + id: parseInt(params.slug) + } }); - db.data.groups.forEach(group => { - delete group.permissions[params.slug]; - }); - - await db.write(); redirect(302, "/dashboard/devices"); + }, + wake: async ({ params, locals: { guard } }) => { + console.log("Trying to wake " + params.slug); + + guard = guard.requiresAuth(); + + if (guard.isFailed()) { + 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); + + 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 + }, () => {}); } } \ No newline at end of file diff --git a/src/routes/dashboard/devices/[slug]/+page.svelte b/src/routes/dashboard/devices/[slug]/+page.svelte index c7ea642..9787224 100644 --- a/src/routes/dashboard/devices/[slug]/+page.svelte +++ b/src/routes/dashboard/devices/[slug]/+page.svelte @@ -1,35 +1,51 @@ -
- - - - - - - - {#if data.device} - - {/if} -
- -{#if form?.error} -

Could not create device: {form.error}

-{/if} \ No newline at end of file + +
+ + + + + + + + + + + + +
+ + {#if data.device} + + {/if} +
+ +
\ No newline at end of file diff --git a/src/routes/dashboard/groups/+page.server.ts b/src/routes/dashboard/groups/+page.server.ts index ff3813e..31990b5 100644 --- a/src/routes/dashboard/groups/+page.server.ts +++ b/src/routes/dashboard/groups/+page.server.ts @@ -1,10 +1,15 @@ -import { db } from "$lib/server/db/db"; +import { prisma } from "$lib/server/db/db"; import { type ServerLoad } from "@sveltejs/kit"; export const load: ServerLoad = async ({ locals: { guard } }) => { guard.requiresAdmin().orRedirects(); return { - groups: db.data.groups, + groups: await prisma.group.findMany({ + include: { + users: true, + devices: true, + } + }), } }; \ No newline at end of file diff --git a/src/routes/dashboard/groups/+page.svelte b/src/routes/dashboard/groups/+page.svelte index 7ad42ae..39029e9 100644 --- a/src/routes/dashboard/groups/+page.svelte +++ b/src/routes/dashboard/groups/+page.svelte @@ -1,16 +1,33 @@ -new group + + {#snippet actionSnippet()} + + {/snippet} -{#if data.groups.length === 0} -

No groups found

-{:else} -

Groups:

-
    - {#each data.groups as group} -
  • {group.name} - edit
  • - {/each} -
-{/if} \ No newline at end of file + {#snippet contentSnippet()} + {#if data.groups.length === 0} +

No groups found

+ {:else} +
+ {#each data.groups as group} + + {#snippet actionsSnippet()} + {#if data.user.admin} +
+ {/if} + {/snippet} +
\ No newline at end of file diff --git a/src/routes/dashboard/groups/[slug]/+page.server.ts b/src/routes/dashboard/groups/[slug]/+page.server.ts index 6b0fe4d..f0dfd7e 100644 --- a/src/routes/dashboard/groups/[slug]/+page.server.ts +++ b/src/routes/dashboard/groups/[slug]/+page.server.ts @@ -1,21 +1,23 @@ -import { db } from "$lib/server/db/db"; -import type { Permission } from "$lib/server/db/types/permission"; -import type { User } from "$lib/server/db/types/user"; -import { getUserFromSession } from "$lib/server/sessions"; +import { prisma } from "$lib/server/db/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 = db.data.groups.find(group => group.id === params.slug); + const group = await prisma.group.findUnique({ + where: { + id: parseInt(params.slug!) || -1, + }, include: { + devices: true + } + }); if (!group && params.slug !== "new") { redirect(302, "/dashboard/groups"); } return { - devices: db.data.devices, + devices: await prisma.device.findMany(), group, } }; @@ -28,20 +30,7 @@ export const actions: Actions = { const form = await request.formData(); const name = form.get("name")?.toString(); - - let permissions: { [key: string]: Permission } = {}; - - for (let deviceId of form.getAll("canSee")) { - if (db.data.devices.find(device => device.id === deviceId)) { - permissions[deviceId.toString()] = { wake: false }; - } - } - - for (let deviceId of form.getAll("canWake")) { - if (db.data.devices.find(device => device.id === deviceId)) { - permissions[deviceId.toString()] = { wake: true }; - } - } + const devices = form.getAll("devices").map(d => ({ id: parseInt(d.toString()) })); if (!name) { // TODO better validation @@ -51,23 +40,25 @@ export const actions: Actions = { } if (params.slug === "new") { - await db.update(({ groups }) => { - groups.push({ - id: nanoid(), + await prisma.group.create({ + data: { name, - permissions, - }); + devices: { + connect: devices + } + } }); } else { - await db.update(({ groups }) => { - const group = groups.find(group => group.id === params.slug); - - if (!group) { - return; + await prisma.group.update({ + where: { + id: parseInt(params.slug!) || -1, + }, + data: { + name, + devices: { + set: devices + } } - - group.name = name; - group.permissions = permissions; }); } @@ -79,14 +70,12 @@ export const actions: Actions = { return fail(403); } - db.data.groups = db.data.groups.filter(group => group.id !== params.slug); - db.data.users = db.data.users.map(user => { - user.groups = user.groups.filter(groupId => groupId !== params.slug); - return user; + await prisma.group.delete({ + where: { + id: parseInt(params.slug!) || -1, + } }); - await db.write(); - redirect(302, "/dashboard/groups"); } }; \ No newline at end of file diff --git a/src/routes/dashboard/groups/[slug]/+page.svelte b/src/routes/dashboard/groups/[slug]/+page.svelte index d4b2eef..d4c61c4 100644 --- a/src/routes/dashboard/groups/[slug]/+page.svelte +++ b/src/routes/dashboard/groups/[slug]/+page.svelte @@ -1,34 +1,37 @@ -
- - - - - {#if data.group} - - {/if} -
+ +
+ -{#if form?.error} -

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

-{/if} \ No newline at end of file + ({ + value: d.id.toString(), + name: d.name, + selected: data.group?.devices.find(gd => gd.id === d.id) ? true : false }))}/> + +
+ + {#if data.group} + + {/if} +
+ +
\ No newline at end of file diff --git a/src/routes/dashboard/users/+page.server.ts b/src/routes/dashboard/users/+page.server.ts index f8b5abb..f2e20a9 100644 --- a/src/routes/dashboard/users/+page.server.ts +++ b/src/routes/dashboard/users/+page.server.ts @@ -1,10 +1,15 @@ -import { db } from "$lib/server/db/db"; +import { prisma } from "$lib/server/db/db"; import type { ServerLoad } from "@sveltejs/kit"; export const load: ServerLoad = async ({ locals: { guard } }) => { guard.requiresAdmin().orRedirects(); return { - users: db.data.users, + users: await prisma.user.findMany({ + include: { + groups: true, + devices: true + } + }), } }; \ No newline at end of file diff --git a/src/routes/dashboard/users/+page.svelte b/src/routes/dashboard/users/+page.svelte index ea48b7f..1ba6738 100644 --- a/src/routes/dashboard/users/+page.svelte +++ b/src/routes/dashboard/users/+page.svelte @@ -1,16 +1,27 @@ -new user + + {#snippet actionSnippet()} + + {/snippet} -{#if data.users.length === 0} -

No users found

-{:else} -

Users:

-
    - {#each data.users as user} -
  • {user.name} - edit
  • - {/each} -
-{/if} \ No newline at end of file + {#snippet contentSnippet()} +
+ {#each data.users as user} + + {#snippet actionsSnippet()} +
+ {/snippet} +
\ No newline at end of file diff --git a/src/routes/dashboard/users/[slug]/+page.server.ts b/src/routes/dashboard/users/[slug]/+page.server.ts index 196415e..3e0d3f7 100644 --- a/src/routes/dashboard/users/[slug]/+page.server.ts +++ b/src/routes/dashboard/users/[slug]/+page.server.ts @@ -1,13 +1,19 @@ -import { db } from "$lib/server/db/db"; -import type { Permission } from "$lib/server/db/types/permission"; +import { prisma } from "$lib/server/db/db"; import { fail, redirect, type Actions } from "@sveltejs/kit"; import bcrypt from "bcryptjs"; -import { nanoid } from "nanoid"; export const load = async ({ locals: { guard }, params }) => { guard.requiresAdmin().orRedirects(); - const user = db.data.users.find(user => user.id === params.slug); + const user = await prisma.user.findUnique({ + where: { + id: parseInt(params.slug!) || -1, + }, + include: { + groups: true, + devices: true + } + }); if (!user && params.slug !== "new") { redirect(302, "/dashboard/users"); @@ -15,8 +21,8 @@ export const load = async ({ locals: { guard }, params }) => { return { user, - groups: db.data.groups, - devices: db.data.devices, + groups: await prisma.group.findMany(), + devices: await prisma.device.findMany(), } }; @@ -30,23 +36,8 @@ export const actions: Actions = { const name = form.get("name")?.toString(); const admin = form.get("admin")?.toString() === "on"; const password = form.get("password")?.toString() ?? ""; - const groups = form.getAll("groups") - .map(groupId => groupId.toString()) - .filter(groupId => db.data.groups.find(group => group.id === groupId)); - - let permissions: { [key: string]: Permission } = {}; - - for (let deviceId of form.getAll("canSee")) { - if (db.data.devices.find(device => device.id === deviceId)) { - permissions[deviceId.toString()] = { wake: false }; - } - } - - for (let deviceId of form.getAll("canWake")) { - if (db.data.devices.find(device => device.id === deviceId)) { - permissions[deviceId.toString()] = { wake: true }; - } - } + const groups = form.getAll("groups").map(g => ({ id: parseInt(g.toString()) })); + const devices = form.getAll("devices").map(d => ({ id: parseInt(d.toString()) })); if (!name) { // TODO better validation @@ -56,33 +47,40 @@ export const actions: Actions = { } if (params.slug === "new") { - if (password.length < 8) { + if (password.length < 4) { return { error: "PASSWORD_TOO_WEAK" } } - await db.update(({ users }) => { - users.push({ - id: nanoid(), + await prisma.user.create({ + data: { name, password: bcrypt.hashSync(password, 10), admin, - groups, - permissions - }); + groups: { + connect: groups + }, + devices: { + connect: devices + } + } }); } else { - await db.update(({ users }) => { - const user = users.find(user => user.id === params.slug); - if (user) { - user.name = name; - user.admin = admin; - if (password !== "") { - user.password = bcrypt.hashSync(password, 10); - } - user.groups = groups; - user.permissions = permissions; + 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 } }); } diff --git a/src/routes/dashboard/users/[slug]/+page.svelte b/src/routes/dashboard/users/[slug]/+page.svelte index 2da0a2c..090fc67 100644 --- a/src/routes/dashboard/users/[slug]/+page.svelte +++ b/src/routes/dashboard/users/[slug]/+page.svelte @@ -1,50 +1,60 @@ -
- - - - - - - - {#if data.user} - - {/if} -
+ +
+ -{#if form?.error} -

Could not update user: {form.error}

-{/if} \ No newline at end of file + + + + + ({ + value: g.id.toString(), + name: g.name, + selected: data.user?.groups.find(ug => ug.id === g.id) ? true : false }))}/> + + ({ + value: d.id.toString(), + name: d.name, + selected: data.user?.devices.find(ud => ud.id === d.id) ? true : false }))}/> + +
+ + {#if data.user} + + {/if} +
+ +
\ No newline at end of file diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index 1d8060c..144017a 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -1,7 +1,7 @@ import { createSession, getUserFromSession } from '$lib/server/sessions'; import { redirect } from '@sveltejs/kit'; import type { Actions } from './$types'; -import { db } from '$lib/server/db/db'; +import { prisma } from '$lib/server/db/db'; import bcrypt from 'bcryptjs'; export const actions = { @@ -20,7 +20,11 @@ export const actions = { } } - const user = db.data.users.find(user => user.name === username); + const user = await prisma.user.findUnique({ + where: { + name: username + } + }); if (!user || !bcrypt.compareSync(password, user.password)) { return { diff --git a/vite.config.ts b/vite.config.ts index 2d35c4f..637b653 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; +import Icons from 'unplugin-icons/vite'; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()] + plugins: [tailwindcss(), sveltekit(), Icons({ compiler: 'svelte' })], });