Compare commits

..

7 Commits

Author SHA1 Message Date
3782f7b4fe refactor: replace nullish .find() checks with .some() 2025-04-21 00:39:05 +02:00
1782e68984 fix: mobile layout white spaces / underlying body showing 2025-04-21 00:31:07 +02:00
de1184b742 fix: navbar must only display devices if non-admin
also changed mobile layout a tiny bit
2025-04-21 00:26:44 +02:00
0ea3624ecd fix: scrollbar issues
untested on windows - todo
2025-04-20 22:59:50 +02:00
2f0fd05d35 fix: missing empty list placeholder 2025-04-20 22:35:37 +02:00
456131602f fix: reversed unique check 2025-04-20 22:27:40 +02:00
fb44cf9fa3 feat: add necessary bits for deployment 2025-04-19 22:47:01 +02:00
15 changed files with 298 additions and 36 deletions

View File

@ -10,6 +10,14 @@ USE_REVERSE_PROXY=true
# Name of the header containing the request's emitter's IP adress. No effect is USE_REVERSE_PROXY is false.
REAL_IP_HEADER=X-Forwarded-For
# IP and port used on which the built not server will host the site (not used by dev server / npm run dev)
HOST=127.0.0.1
PORT=4000
# Your domain name. If running on a personnal device, use the IP and port
# Domain example : https://mydomain.com, home-hosted example : http://127.0.0.1:4000
ORIGIN=http://127.0.0.1:4000
# Uncomment to choose your own default admin password (you should change it afterwards regardless)
# Else, a random one will be generated and printed to logs and written to ./data/default_admin_pass.txt
# DEFAULT_ADMIN_PASS=changeme

244
package-lock.json generated
View File

@ -23,7 +23,7 @@
},
"devDependencies": {
"@iconify-json/tabler": "^1.2.17",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",
@ -623,6 +623,112 @@
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz",
"integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"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",
@ -912,17 +1018,20 @@
"acorn": "^8.9.0"
}
},
"node_modules/@sveltejs/adapter-auto": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz",
"integrity": "sha512-kmuYSQdD2AwThymQF0haQhM8rE5rhutQXG4LNbnbShwhMO4qQGnKaaTy+88DuNSuoQDi58+thpq8XpHc1+oEKQ==",
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"import-meta-resolve": "^4.1.0"
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"rollup": "^4.9.5"
},
"peerDependencies": {
"@sveltejs/kit": "^2.0.0"
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/kit": {
@ -1271,6 +1380,13 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/validator": {
"version": "13.12.3",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.3.tgz",
@ -1375,6 +1491,13 @@
"node": ">=6"
}
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@ -1513,6 +1636,13 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/exsolve": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.4.tgz",
@ -1550,6 +1680,16 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz",
@ -1583,6 +1723,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/humanize-duration": {
"version": "3.32.1",
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.1.tgz",
@ -1606,6 +1759,29 @@
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true,
"license": "MIT"
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@ -2025,6 +2201,13 @@
"quansync": "^0.2.7"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@ -2039,6 +2222,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
@ -2157,6 +2353,27 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@ -2288,6 +2505,19 @@
"inline-style-parser": "0.2.4"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svelte": {
"version": "5.25.7",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.25.7.tgz",

View File

@ -15,7 +15,7 @@
},
"devDependencies": {
"@iconify-json/tabler": "^1.2.17",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",

View File

@ -3,4 +3,9 @@
* {
font-family: 'Inter', sans-serif;
scrollbar-color: var(--color-neutral-600) transparent;
}
body {
background-color: var(--color-neutral-800);
}

View File

@ -2,7 +2,7 @@ import { env } from '$env/dynamic/private';
import { initRepos, users } from '$lib/server/db';
import { Guard } from '$lib/server/guard';
import { getUserFromSession } from '$lib/server/sessions';
import type { Handle, ServerInit } from '@sveltejs/kit';
import type { Handle, HandleServerError, ServerInit } from '@sveltejs/kit';
import bcrypt from 'bcryptjs';
import { writeFileSync } from 'fs';
import { nanoid } from 'nanoid';
@ -39,3 +39,10 @@ export const handle: Handle = async ({ event, resolve }) => {
return await resolve(event);
};
export const handleError: HandleServerError = async ({ status, error }) => {
// silence 404s, mostly for browser trying to fetch a favicon by default
if (status != 404) {
console.error(error);
}
};

View File

@ -21,7 +21,7 @@ export class LowDeviceRepo implements IDeviceRepo {
}
async create(device: New<Device>): Promise<DeviceError | undefined> {
if (this.db.data.devices.find((d) => d.name === device.name)) {
if (this.db.data.devices.some((d) => d.name === device.name)) {
return DeviceErrors.DUPLICATE_NAME;
}
@ -33,7 +33,7 @@ export class LowDeviceRepo implements IDeviceRepo {
}
async update(device: Updated<Device>): Promise<DeviceError | undefined> {
if (this.db.data.devices.find((d) => d.name === device.name && d.id !== device.id)) {
if (this.db.data.devices.some((d) => d.name === device.name && d.id !== device.id)) {
return DeviceErrors.DUPLICATE_NAME;
}

View File

@ -21,7 +21,7 @@ export class LowGroupRepo implements IGroupRepo {
}
async create(group: New<Group>): Promise<GroupError | undefined> {
if (this.db.data.groups.find((g) => g.name === group.name)) {
if (this.db.data.groups.some((g) => g.name === group.name)) {
return GroupErrors.DUPLICATE_NAME;
}

View File

@ -29,7 +29,7 @@ export class LowUserRepo implements IUserRepo {
}
async create(user: New<User>) {
if (!this.db.data.users.find((u) => u.name == user.name)) {
if (this.db.data.users.some((u) => u.name == user.name)) {
return UserErrors.DUPLICATE_NAME;
}
@ -51,7 +51,7 @@ export class LowUserRepo implements IUserRepo {
}
async update(user: Updated<User>) {
if (this.db.data.users.find((u) => u.name == user.name && u.id != user.id)) {
if (this.db.data.users.some((u) => u.name == user.name && u.id != user.id)) {
return UserErrors.DUPLICATE_NAME;
}

View File

@ -2,7 +2,7 @@
import { Button } from 'bits-ui';
import IconPlus from '~icons/tabler/plus';
let { createHref = null, msgAdd = '', children } = $props();
let { createHref = null, msgAdd = '', empty = false, children } = $props();
</script>
{#if createHref}
@ -21,7 +21,8 @@
<div class="w-full sm:px-6 px-3 py-4 max-w-3xl mx-auto">
{#if children}
{@render children?.()}
{:else}
{/if}
{#if empty}
<div class="p-4 text-center text-neutral-700">
<p>Nothing here.</p>
{#if createHref}

View File

@ -4,10 +4,11 @@
let { Icon = null, children, active, ...others } = $props();
</script>
<li role="none">
<li role="none" class="grow-1">
<Button.Root
role="menuitem"
class="flex items-center cursor-pointer px-4 py-1 rounded-full transition-all duration-300 ease-in-out
class="flex items-center justify-center cursor-pointer px-4 py-1 rounded-full
transition-all duration-300 ease-in-out
{active ? 'bg-neutral-100 text-neutral-900' : 'hover:bg-neutral-600'}"
{...others}
>

View File

@ -34,10 +34,12 @@
<NavBarLink Icon={IconHome} active={isActive('devices')} href="/dash/devices"
>Devices</NavBarLink
>
<NavBarLink Icon={IconUsers} active={isActive('users')} href="/dash/users">Users</NavBarLink>
<NavBarLink Icon={IconUsersGroup} active={isActive('groups')} href="/dash/groups"
>Groups</NavBarLink
>
{#if data.user.admin}
<NavBarLink Icon={IconUsers} active={isActive('users')} href="/dash/users">Users</NavBarLink>
<NavBarLink Icon={IconUsersGroup} active={isActive('groups')} href="/dash/groups"
>Groups</NavBarLink
>
{/if}
</NavBar>
<div class="flex grow">
<div
@ -97,6 +99,6 @@
</div>
</div>
<div class="w-full h-svh bg-neutral-900 overflow-y-scroll sm:pt-20 pt-28">
<div class="w-full h-dvh bg-neutral-900 overflow-y-auto sm:pt-20 pt-28">
{@render children()}
</div>

View File

@ -28,7 +28,11 @@
});
</script>
<ListPage createHref={data.user.admin ? '/dash/devices/new' : null} msgAdd="Add Device">
<ListPage
createHref={data.user.admin ? '/dash/devices/new' : null}
empty={data.devices.length == 0}
msgAdd="Add Device"
>
<div class="flex gap-4 flex-wrap justify-center">
{#each data.devices as device}
<ResCard

View File

@ -8,7 +8,11 @@
store.pageTitle = 'Listing all groups';
</script>
<ListPage createHref={data.user.admin ? '/dash/groups/new' : null} msgAdd="Add Group">
<ListPage
createHref={data.user.admin ? '/dash/groups/new' : null}
empty={data.groups.length == 0}
msgAdd="Add Group"
>
<div class="flex gap-4 flex-wrap justify-center">
{#each data.groups as group}
<ResCard

View File

@ -28,7 +28,7 @@
<title>{env.PUBLIC_SITE_NAME} - Sign In</title>
</svelte:head>
<div class="flex items-center justify-center w-svw h-svh bg-neutral-900">
<div class="flex items-center justify-center w-svw h-dvh bg-neutral-900">
<div
class="flex flex-col m-4 w-full max-w-120 bg-neutral-950 p-8 sm:p-12 sm:px
rounded-2xl border border-neutral-700 shadow-xl"

View File

@ -1,18 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
},
};
export default config;