feat & styling
This commit is contained in:
parent
c753171846
commit
c419d57754
5
.gitignore
vendored
5
.gitignore
vendored
@ -24,4 +24,7 @@ vite.config.ts.timestamp-*
|
||||
|
||||
# Have empty data folder ready to go
|
||||
/data/*
|
||||
!/data/.gitkeep
|
||||
!/data/.gitkeep
|
||||
|
||||
# Generated prisma client
|
||||
generated/prisma/
|
||||
439
package-lock.json
generated
439
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
77
prisma/migrations/20250408121716_init/migration.sql
Normal file
77
prisma/migrations/20250408121716_init/migration.sql
Normal file
@ -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");
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -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"
|
||||
38
prisma/schema.prisma
Normal file
38
prisma/schema.prisma
Normal file
@ -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[]
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
1
src/app.d.ts
vendored
1
src/app.d.ts
vendored
@ -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 {
|
||||
|
||||
@ -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}`);
|
||||
|
||||
51
src/lib/components/forms/InputSelect.svelte
Normal file
51
src/lib/components/forms/InputSelect.svelte
Normal file
@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import Button from "../ui/Button.svelte";
|
||||
import IconPlus from "~icons/tabler/plus";
|
||||
import IconMinus from "~icons/tabler/minus";
|
||||
|
||||
let {
|
||||
label,
|
||||
sublabel = "",
|
||||
name,
|
||||
data
|
||||
}: {
|
||||
label: string,
|
||||
sublabel?: string,
|
||||
name: string,
|
||||
data: {
|
||||
value: string,
|
||||
name: string,
|
||||
selected: boolean
|
||||
}[]
|
||||
} = $props();
|
||||
|
||||
let selectData = $state(data);
|
||||
|
||||
$effect(() => {
|
||||
console.log(selectData);
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex gap-x-10 gap-y-3 flex-wrap mb-5">
|
||||
<label for={name} class="block w-1/3 min-w-[300px]">
|
||||
<p>{label}</p>
|
||||
{#if sublabel}
|
||||
<p class="text-sm text-neutral-400">{sublabel}</p>
|
||||
{/if}
|
||||
</label>
|
||||
<select multiple name={name} class="hidden">
|
||||
{#each selectData.filter(d => d.selected) as el}
|
||||
<option value={el.value} selected></option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<div class="flex gap-2 flex-wrap border border-neutral-200 rounded p-4 shadow w-fit h-fit max-w-[300px] max-h-[200px] overflow-y-scroll">
|
||||
{#if selectData.length == 0}
|
||||
<p class="text-sm text-neutral-400">No data</p>
|
||||
{:else}
|
||||
{#each selectData as el}
|
||||
<Button Icon={el.selected ? IconMinus : IconPlus} type="button" extra="!px-2 !py-1 !text-xs !rounded-sm" onclick={() => {el.selected = !el.selected}} inverted={!el.selected}>{el.name}</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
20
src/lib/components/forms/InputText.svelte
Normal file
20
src/lib/components/forms/InputText.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
label,
|
||||
sublabel = "",
|
||||
name,
|
||||
defaultValue,
|
||||
...others
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex gap-x-10 gap-y-3 flex-wrap mb-5 items-center">
|
||||
<label for={name} class="block w-1/3 min-w-[300px]">
|
||||
<p>{label}</p>
|
||||
{#if sublabel}
|
||||
<p class="text-sm text-neutral-400">{sublabel}</p>
|
||||
{/if}
|
||||
</label>
|
||||
<input id={name} name={name} type="text" defaultValue={defaultValue} {...others}
|
||||
class="block h-fit rounded border border-gray-300 shadow-sm px-4 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
26
src/lib/components/forms/InputToggle.svelte
Normal file
26
src/lib/components/forms/InputToggle.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import Button from "../ui/Button.svelte";
|
||||
import IconCheck from "~icons/tabler/check";
|
||||
import IconX from "~icons/tabler/x";
|
||||
|
||||
let {
|
||||
label,
|
||||
sublabel = "",
|
||||
name,
|
||||
checked,
|
||||
options = ["Yes", "No"],
|
||||
...others
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex gap-x-10 gap-y-3 flex-wrap mb-5 items-center">
|
||||
<label for={name} class="block w-1/3 min-w-[300px]">
|
||||
<p>{label}</p>
|
||||
{#if sublabel.length > 0}
|
||||
<p class="text-sm text-neutral-400">{sublabel}</p>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<input class="hidden" type="checkbox" id={name} name={name} defaultChecked={checked}/>
|
||||
<Button Icon={checked ? IconCheck : IconX} type="button" inverted={!checked} {...others} onclick={() => checked = !checked}>{checked ? options[0] : options[1]}</Button>
|
||||
</div>
|
||||
19
src/lib/components/resources/ResourceCard.svelte
Normal file
19
src/lib/components/resources/ResourceCard.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
subtitle,
|
||||
actionsSnippet
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex gap-10 justify-between items-center rounded border border-neutral-200 py-4 px-6 shadow">
|
||||
<div>
|
||||
<p class="font-medium">{title}</p>
|
||||
<p class="text-neutral-400">{subtitle}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{#if actionsSnippet}
|
||||
{@render actionsSnippet()}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
38
src/lib/components/resources/ResourceEditPage.svelte
Normal file
38
src/lib/components/resources/ResourceEditPage.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import Button from "../ui/Button.svelte";
|
||||
import HorizontalSpacer from "../ui/HorizontalSpacer.svelte";
|
||||
import ResourcePageHeader from "./ResourcePageHeader.svelte";
|
||||
import IconEclamationCircle from "~icons/tabler/exclamation-circle";
|
||||
import IconChevronLeft from "~icons/tabler/chevron-left";
|
||||
|
||||
let {
|
||||
heading,
|
||||
children,
|
||||
error = null,
|
||||
} = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (error) {
|
||||
setTimeout(() => error = null, 5000)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<ResourcePageHeader title={heading}>
|
||||
<Button Icon={IconChevronLeft} onclick={() => history.back()} inverted>Back</Button>
|
||||
</ResourcePageHeader>
|
||||
|
||||
<HorizontalSpacer/>
|
||||
|
||||
<div class="text-sm text-neutral-700 flex-1 overflow-scroll">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div transition:fade>
|
||||
<Button Icon={IconEclamationCircle} type="button" color="red" onclick={() => error = null}>{error} (Click to dismiss)</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
22
src/lib/components/resources/ResourceListPage.svelte
Normal file
22
src/lib/components/resources/ResourceListPage.svelte
Normal file
@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import HorizontalSpacer from "../ui/HorizontalSpacer.svelte";
|
||||
import ResourcePageHeader from "./ResourcePageHeader.svelte";
|
||||
|
||||
let {
|
||||
heading,
|
||||
actionSnippet,
|
||||
contentSnippet
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<ResourcePageHeader title={heading}>
|
||||
{@render actionSnippet()}
|
||||
</ResourcePageHeader>
|
||||
|
||||
<HorizontalSpacer/>
|
||||
|
||||
<div class="text-sm text-neutral-700 flex-1 overflow-scroll">
|
||||
{@render contentSnippet()}
|
||||
</div>
|
||||
</div>
|
||||
8
src/lib/components/resources/ResourcePageHeader.svelte
Normal file
8
src/lib/components/resources/ResourcePageHeader.svelte
Normal file
@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
let { title, children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-xl font-semibold">{title}</h1>
|
||||
{@render children()}
|
||||
</div>
|
||||
73
src/lib/components/ui/Button.svelte
Normal file
73
src/lib/components/ui/Button.svelte
Normal file
@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import type { ClassValue, HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
|
||||
type Props = {
|
||||
a?: boolean,
|
||||
color?: "neutral" | "red" | "green",
|
||||
inverted?: boolean,
|
||||
Icon?: any,
|
||||
extra?: ClassValue,
|
||||
children?: any
|
||||
} & HTMLButtonAttributes & HTMLAnchorAttributes;
|
||||
|
||||
let {
|
||||
a,
|
||||
color,
|
||||
inverted,
|
||||
Icon,
|
||||
children,
|
||||
extra,
|
||||
...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 fullClasses = $derived(baseClasses + " " + extra + " " + colorClasses);
|
||||
|
||||
$effect(() => {
|
||||
if (!inverted) {
|
||||
switch (color) {
|
||||
case "red":
|
||||
colorClasses = "bg-red-500 hover:bg-red-600 text-white border border-transparent";
|
||||
break;
|
||||
case "green":
|
||||
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";
|
||||
}
|
||||
} else {
|
||||
switch (color) {
|
||||
case "red":
|
||||
colorClasses = "bg-white hover:bg-red-100 text-red-500 border border-red-500";
|
||||
break;
|
||||
case "green":
|
||||
colorClasses = "bg-white hover:bg-emerald-100 text-emerald-500 border border-emerald-500";
|
||||
break;
|
||||
default:
|
||||
colorClasses = "bg-white hover:bg-neutral-100 text-neutral-800 border border-neutral-800";
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if a}
|
||||
<a class={fullClasses} {...others}>
|
||||
{#if Icon}
|
||||
<Icon class={children ? "mr-2" : ""}></Icon>
|
||||
{/if}
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button class={fullClasses} {...others}>
|
||||
{#if Icon}
|
||||
<Icon class={children ? "mr-2" : ""}></Icon>
|
||||
{/if}
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
19
src/lib/components/ui/Collapsible.svelte
Normal file
19
src/lib/components/ui/Collapsible.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import IconChevronRight from "~icons/tabler/chevron-right";
|
||||
import { slide } from "svelte/transition";
|
||||
let { children, label } = $props();
|
||||
let expanded = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="mb-4">
|
||||
<button type="button" class="flex items-center gap-2 cursor-pointer" onclick={() => expanded = !expanded}>
|
||||
<IconChevronRight class="{expanded ? "rotate-90" : ""} transition-all duration-300"/>
|
||||
<h1 class="text-xs uppercase font-light">{label}</h1>
|
||||
</button>
|
||||
|
||||
{#if expanded}
|
||||
<div transition:slide={{duration: 300}} class="mt-4">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
1
src/lib/components/ui/HorizontalSpacer.svelte
Normal file
1
src/lib/components/ui/HorizontalSpacer.svelte
Normal file
@ -0,0 +1 @@
|
||||
<div class="w-full h-px bg-neutral-200 my-4"></div>
|
||||
@ -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>("./data/db.json", defaultData);
|
||||
//export const db = await JSONFilePreset<Data>("./data/db.json", defaultData);
|
||||
@ -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!;
|
||||
}
|
||||
}
|
||||
@ -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<User | undefined> {
|
||||
export async function getUserFromSession(sessionId?: string) {
|
||||
if (!sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
@ -29,7 +29,11 @@ export async function getUserFromSession(sessionId?: string): Promise<User | und
|
||||
|
||||
// what in the nested fuck is this shit
|
||||
// I thought ORMs made it easier but they just make queries more ridiculous
|
||||
const user = db.data.users.find(user => user.id === data.userId);
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: data.userId
|
||||
}
|
||||
}) ?? undefined;
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
@ -1,15 +1,60 @@
|
||||
<script lang="ts">
|
||||
import IconHome from "~icons/tabler/home";
|
||||
import IconUsers from "~icons/tabler/users";
|
||||
import IconUsersGroup from "~icons/tabler/users-group";
|
||||
import IconLogout2 from "~icons/tabler/logout2";
|
||||
|
||||
let { children, data } = $props();
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'Devices',
|
||||
href: '/dashboard/devices',
|
||||
icon: IconHome,
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
name: 'Users',
|
||||
href: '/dashboard/users',
|
||||
icon: IconUsers,
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: 'Groups',
|
||||
href: '/dashboard/groups',
|
||||
icon: IconUsersGroup,
|
||||
admin: true,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<p>logged in as {data.user.name}</p>
|
||||
<a href="/dashboard/devices">devices</a>
|
||||
{#if data.user.admin}
|
||||
<a href="/dashboard/users">users</a>
|
||||
<a href="/dashboard/groups">groups</a>
|
||||
{/if}
|
||||
<a href="/logout">logout</a>
|
||||
<div class="flex min-h-screen overflow-hidden">
|
||||
<!-- Sidebar -->
|
||||
<div id="sidebar" class="h-screen w-64 bg-neutral-100 transform translate-x-0 transition-transform duration-300 ease-in-out z-40 text-neutral-700 text-sm border-r border-neutral-200">
|
||||
<div class="p-4 flex flex-col h-full">
|
||||
<ul class="flex-1">
|
||||
{#each links as link}
|
||||
{#if data.user.admin || !link.admin}
|
||||
<li class="mb-2">
|
||||
<a class="block flex items-center p-2 bg-transparent hover:bg-neutral-200 transition-all cursor-pointer rounded" href={link.href}>
|
||||
<link.icon class="mr-2"/>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<br><br>
|
||||
|
||||
{@render children()}
|
||||
<div class="p-2 flex w-full justify-between items-center">
|
||||
<p>Signed in as <span class="underline underline-offset-4">{data.user.name}</span></p>
|
||||
<a data-sveltekit-preload-data="tap" href="/logout" class="block p-2 bg-transparent hover:bg-neutral-200 transition-all cursor-pointer rounded"><IconLogout2/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 p-6 bg-white w-full h-screen">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
};
|
||||
@ -1,18 +1,40 @@
|
||||
<script lang="ts">
|
||||
import IconPlus from "~icons/tabler/plus";
|
||||
import IconEdit from "~icons/tabler/edit";
|
||||
import IconPlayerPlay from "~icons/tabler/player-play";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
|
||||
import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
|
||||
import { enhance } from "$app/forms";
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
{#if data.user.admin}
|
||||
<a href="/dashboard/devices/new">new device</a>
|
||||
{/if}
|
||||
<ResourceListPage heading="Devices">
|
||||
{#snippet actionSnippet()}
|
||||
{#if data.user.admin}
|
||||
<Button Icon={IconPlus} a href="/dashboard/devices/new">Add Device</Button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if data.devices.length === 0}
|
||||
<p>No devices found</p>
|
||||
{:else}
|
||||
<p>Devices:</p>
|
||||
<ul>
|
||||
{#each data.devices as device}
|
||||
<li>{device.name} {#if data.user.admin}- <a href="/dashboard/devices/{device.id}">edit</a>{/if}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#snippet contentSnippet()}
|
||||
{#if data.devices.length === 0}
|
||||
<p>No devices found</p>
|
||||
{:else}
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
{#each data.devices as device}
|
||||
<ResourceCard title={device.name} subtitle={device.mac}>
|
||||
{#snippet actionsSnippet()}
|
||||
{#if data.user.admin}
|
||||
<Button Icon={IconEdit} a href="/dashboard/devices/{device.id}" extra="!p-2"/>
|
||||
{/if}
|
||||
<form method="POST" action="/dashboard/devices/{device.id}?/wake" use:enhance>
|
||||
<Button Icon={IconPlayerPlay} type="submit" color="green" extra="!p-2"></Button>
|
||||
</form>
|
||||
{/snippet}
|
||||
</ResourceCard>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ResourceListPage>
|
||||
@ -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
|
||||
}, () => {});
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from "$app/forms";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import Collapsible from "$lib/components/ui/Collapsible.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";
|
||||
|
||||
let { form, data } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/update">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" type="text" defaultValue={data.device ? data.device.name : "New Device"}>
|
||||
</label>
|
||||
<label>
|
||||
Mac Address
|
||||
<input name="mac" type="text" defaultValue={data.device ? data.device.mac : "00:00:00:00:00:00"}>
|
||||
</label>
|
||||
<label>
|
||||
Broadcast Ip Address
|
||||
<input name="ip" type="text" defaultValue={data.device ? data.device.ip : "255.255.255.255"}>
|
||||
</label>
|
||||
<label>
|
||||
Port
|
||||
<input name="port" type="text" defaultValue={data.device ? data.device.port : "9"}>
|
||||
</label>
|
||||
<label>
|
||||
Amount of packets
|
||||
<input name="packets" type="text" defaultValue={data.device ? data.device.packets : "3"}>
|
||||
</label>
|
||||
|
||||
<button>{data.device ? "Update" : "Create"}</button>
|
||||
{#if data.device}
|
||||
<button formaction="?/delete">Delete</button>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
{#if form?.error}
|
||||
<p>Could not create device: {form.error}</p>
|
||||
{/if}
|
||||
<ResourceEditPage heading={data.device ? "Editing Device - " + data.device.name : "New Device"} error={form?.error}>
|
||||
<form method="POST" action="?/update" use:enhance>
|
||||
<InputText
|
||||
label="Device Name"
|
||||
sublabel="How the device will appear in the dashboard"
|
||||
name="name"
|
||||
defaultValue={data.device ? data.device.name : "New Device"}/>
|
||||
|
||||
<InputText
|
||||
label="MAC Address"
|
||||
sublabel="Address used for the Wake-On-Lan packets"
|
||||
name="mac"
|
||||
defaultValue={data.device ? data.device.mac : "00:00:00:00:00:00"}/>
|
||||
|
||||
<Collapsible label="Advanced">
|
||||
<InputText
|
||||
label="Broadcast IP Address"
|
||||
name="broadcast"
|
||||
defaultValue={data.device ? data.device.broadcast : "255.255.255.255"}/>
|
||||
|
||||
<InputText
|
||||
label="Broadcast Port"
|
||||
name="port"
|
||||
defaultValue={data.device ? data.device.port : "9"}/>
|
||||
|
||||
<InputText
|
||||
label="Packets Quantity"
|
||||
name="packets"
|
||||
defaultValue={data.device ? data.device.packets : "3"}/>
|
||||
</Collapsible>
|
||||
|
||||
<div class="flex gap-5 items-center">
|
||||
<Button Icon={IconDeviceFloppy}>{data.device ? "Update" : "Create"}</Button>
|
||||
{#if data.device}
|
||||
<Button Icon={IconTrash} color="red" inverted formaction="?/delete">Delete</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</ResourceEditPage>
|
||||
@ -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,
|
||||
}
|
||||
}),
|
||||
}
|
||||
};
|
||||
@ -1,16 +1,33 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
|
||||
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
|
||||
import IconPlus from "~icons/tabler/plus";
|
||||
import IconEdit from "~icons/tabler/edit";
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<a href="/dashboard/groups/new">new group</a>
|
||||
<ResourceListPage heading="Groups">
|
||||
{#snippet actionSnippet()}
|
||||
<Button Icon={IconPlus} a href="/dashboard/groups/new">Add Group</Button>
|
||||
{/snippet}
|
||||
|
||||
{#if data.groups.length === 0}
|
||||
<p>No groups found</p>
|
||||
{:else}
|
||||
<p>Groups:</p>
|
||||
<ul>
|
||||
{#each data.groups as group}
|
||||
<li>{group.name} - <a href="/dashboard/groups/{group.id}">edit</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#snippet contentSnippet()}
|
||||
{#if data.groups.length === 0}
|
||||
<p>No groups found</p>
|
||||
{: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">
|
||||
{#snippet actionsSnippet()}
|
||||
{#if data.user.admin}
|
||||
<Button Icon={IconEdit} a href="/dashboard/groups/{group.id}" extra="!p-2"/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ResourceCard>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ResourceListPage>
|
||||
@ -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");
|
||||
}
|
||||
};
|
||||
@ -1,34 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from "$app/forms";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import InputSelect from "$lib/components/forms/InputSelect.svelte";
|
||||
import InputText from "$lib/components/forms/InputText.svelte";
|
||||
import ResourceEditPage from "$lib/components/resources/ResourceEditPage.svelte";
|
||||
import IconDeviceFloppy from "~icons/tabler/device-floppy";
|
||||
import IconTrash from "~icons/tabler/trash";
|
||||
|
||||
let { form, data } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/update">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" type="text" defaultValue={data.group ? data.group.name : "New Group"}>
|
||||
</label>
|
||||
<label>
|
||||
Can see
|
||||
<select multiple name="canSee">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.group?.permissions[device.id] ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Can wake
|
||||
<select multiple name="canWake">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.group?.permissions[device.id]?.wake ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button>{data.group ? "Update" : "Create"}</button>
|
||||
{#if data.group}
|
||||
<button formaction="?/delete">Delete</button>
|
||||
{/if}
|
||||
</form>
|
||||
<ResourceEditPage heading={data.group ? "Editing Group - " + data.group.name : "New Group"} error={form?.error}>
|
||||
<form method="POST" action="?/update" use:enhance>
|
||||
<InputText
|
||||
label="Group Name"
|
||||
sublabel="How the group will appear in the dashboard"
|
||||
name="name"
|
||||
defaultValue={data.group ? data.group.name : "New Group"}/>
|
||||
|
||||
{#if form?.error}
|
||||
<p>Could not {data.group ? "update" : "create"} group: {form.error}</p>
|
||||
{/if}
|
||||
<InputSelect
|
||||
label="Devices"
|
||||
sublabel="Devices that users who are part of this group will be ble to wake"
|
||||
name="devices"
|
||||
data={data.devices.map(d => ({
|
||||
value: d.id.toString(),
|
||||
name: d.name,
|
||||
selected: data.group?.devices.find(gd => gd.id === d.id) ? true : false }))}/>
|
||||
|
||||
<div class="flex gap-5 items-center">
|
||||
<Button Icon={IconDeviceFloppy}>{data.group ? "Update" : "Create"}</Button>
|
||||
{#if data.group}
|
||||
<Button Icon={IconTrash} color="red" inverted formaction="?/delete">Delete</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</ResourceEditPage>
|
||||
@ -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
|
||||
}
|
||||
}),
|
||||
}
|
||||
};
|
||||
@ -1,16 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import ResourceCard from "$lib/components/resources/ResourceCard.svelte";
|
||||
import ResourceListPage from "$lib/components/resources/ResourceListPage.svelte";
|
||||
import IconPlus from "~icons/tabler/plus";
|
||||
import IconEdit from "~icons/tabler/edit";
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<a href="/dashboard/users/new">new user</a>
|
||||
<ResourceListPage heading="Users">
|
||||
{#snippet actionSnippet()}
|
||||
<Button Icon={IconPlus} a href="/dashboard/users/new">Add User</Button>
|
||||
{/snippet}
|
||||
|
||||
{#if data.users.length === 0}
|
||||
<p>No users found</p>
|
||||
{:else}
|
||||
<p>Users:</p>
|
||||
<ul>
|
||||
{#each data.users as user}
|
||||
<li>{user.name} - <a href="/dashboard/users/{user.id}">edit</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#snippet contentSnippet()}
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
{#each data.users as user}
|
||||
<ResourceCard title={user.name} subtitle="{user.groups.length} groups, {user.devices.length} devices">
|
||||
{#snippet actionsSnippet()}
|
||||
<Button Icon={IconEdit} a href="/dashboard/users/{user.id}" extra="!p-2"/>
|
||||
{/snippet}
|
||||
</ResourceCard>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</ResourceListPage>
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,50 +1,60 @@
|
||||
<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";
|
||||
|
||||
let { data, form } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" action="?/update">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" type="text" defaultValue={data.user ? data.user.name : "New User"}>
|
||||
</label>
|
||||
<label>
|
||||
Password (leave empty for no change)
|
||||
<input name="password" type="password">
|
||||
</label>
|
||||
<label>
|
||||
Is admin
|
||||
<input name="admin" type="checkbox" checked={data.user ? data.user.admin : false}>
|
||||
</label>
|
||||
<label>
|
||||
Groups
|
||||
<select multiple name="groups">
|
||||
{#each data.groups as group}
|
||||
<option value={group.id} selected={data.user?.groups.includes(group.id) ? true : false}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Can see
|
||||
<select multiple name="canSee">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.user?.permissions[device.id] ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Can wake
|
||||
<select multiple name="canWake">
|
||||
{#each data.devices as device}
|
||||
<option value={device.id} selected={data.user?.permissions[device.id]?.wake ? true : false}>{device.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button>{data.user ? "Update" : "Create"}</button>
|
||||
{#if data.user}
|
||||
<button formaction="?/delete">Delete</button>
|
||||
{/if}
|
||||
</form>
|
||||
<ResourceEditPage heading={data.user ? "Editing User - " + data.user.name : "New User"} error={form?.error}>
|
||||
<form method="POST" action="?/update" use:enhance>
|
||||
<InputText
|
||||
label="User Name"
|
||||
sublabel="How the user will appear in the dashboard"
|
||||
name="name"
|
||||
defaultValue={data.user ? data.user.name : "New User"}/>
|
||||
|
||||
{#if form?.error}
|
||||
<p>Could not update user: {form.error}</p>
|
||||
{/if}
|
||||
<InputText
|
||||
label="Password"
|
||||
sublabel="Leave empty for no change"
|
||||
name="password"
|
||||
type="password"
|
||||
defaultValue=""/>
|
||||
|
||||
<InputToggle
|
||||
label="Admin"
|
||||
name="admin"
|
||||
sublabel="Can manage and use all devices, groups and users"
|
||||
checked={data.user?.admin ?? false}/>
|
||||
|
||||
<InputSelect
|
||||
label="Groups"
|
||||
name="groups"
|
||||
sublabel="Groups that the user will be a part of and inherit devices access from"
|
||||
data={data.groups.map(g => ({
|
||||
value: g.id.toString(),
|
||||
name: g.name,
|
||||
selected: data.user?.groups.find(ug => ug.id === g.id) ? true : false }))}/>
|
||||
|
||||
<InputSelect
|
||||
label="Devices"
|
||||
name="devices"
|
||||
sublabel="Devices that the user can wake"
|
||||
data={data.devices.map(d => ({
|
||||
value: d.id.toString(),
|
||||
name: d.name,
|
||||
selected: data.user?.devices.find(ud => ud.id === d.id) ? true : false }))}/>
|
||||
|
||||
<div class="flex gap-5 items-center">
|
||||
<Button Icon={IconDeviceFloppy}>{data.user ? "Update" : "Create"}</Button>
|
||||
{#if data.user}
|
||||
<Button Icon={IconTrash} color="red" inverted formaction="?/delete">Delete</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</ResourceEditPage>
|
||||
@ -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 {
|
||||
|
||||
@ -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' })],
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user