rename permissions to users, add create new user

This commit is contained in:
seriousm4x
2023-08-13 23:17:53 +02:00
parent 870d2dcc49
commit 311684f05f
4 changed files with 336 additions and 202 deletions

View File

@@ -8,12 +8,12 @@
faCog,
faHome,
faPlus,
faKey,
faSwatchbook,
faChevronDown,
faCheck,
faUserPen,
faUserLargeSlash
faUsersGear,
faUserGear,
faDoorOpen
} from '@fortawesome/free-solid-svg-icons';
import { themeChange } from 'theme-change';
import { onMount } from 'svelte';
@@ -101,10 +101,10 @@
{#if $isAdmin}
<li>
<a
href="/permissions"
href="/users"
class="px-4 py-2"
class:active={$page.url.pathname.startsWith('/permissions')}
><Fa icon={faKey} />Permissions</a
class:active={$page.url.pathname.startsWith('/users')}
><Fa icon={faUsersGear} />Users</a
>
</li>
<li>
@@ -138,11 +138,8 @@
</li>
{#if $isAdmin}
<li>
<a
href="/permissions"
class="p-2"
class:active={$page.url.pathname.startsWith('/permissions')}
><Fa icon={faKey} />Permissions</a
<a href="/users" class="p-2" class:active={$page.url.pathname.startsWith('/users')}
><Fa icon={faUsersGear} />Users</a
>
</li>
<li>
@@ -203,7 +200,7 @@
<div class="dropdown dropdown-end">
<label tabindex="-1" class="btn btn-ghost btn-circle avatar" for="avatar">
<div class="w-10 rounded-full" id="avatar">
<img src="{backendUrl}_/images/avatars/avatar{avatar}.svg" alt="Profile pic" />
<img src="{backendUrl}_/images/avatars/avatar{avatar}.svg" alt="Avatar {avatar}" />
</div>
</label>
<ul
@@ -214,11 +211,11 @@
{$isAdmin ? $pocketbase.authStore.model.email : $pocketbase.authStore.model.username}
</li>
<li>
<a href="/account"><Fa icon={faUserPen} />Edit account</a>
<a href="/account"><Fa icon={faUserGear} />Edit account</a>
</li>
<li>
<div on:click={async () => logout()} on:keydown={async () => logout()} role="none">
<Fa icon={faUserLargeSlash} />Logout
<Fa icon={faDoorOpen} />Logout
</div>
</li>
</ul>

View File

@@ -3,4 +3,5 @@ import type { Record } from 'pocketbase';
export type User = Record & {
username: string;
email: string;
avatar: number;
};

View File

@@ -1,188 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { pocketbase, isAdmin, backendUrl } from '$lib/stores/pocketbase';
import PageLoading from '$lib/components/PageLoading.svelte';
import toast from 'svelte-french-toast';
import Fa from 'svelte-fa';
import { faSave } from '@fortawesome/free-solid-svg-icons';
import type { User } from '$lib/types/user';
import type { Device } from '$lib/types/device';
import type { Permission } from '$lib/types/permission';
let users = [] as User[];
let permissions = [] as Permission[];
let devices = [] as Device[];
$: users.map((user) => {
if (!permissions.find((perm) => perm.user === user.id)) {
permissions = [...permissions, { user: user.id } as Permission];
}
});
onMount(() => {
if (!$isAdmin) {
toast(`You don't have permission to visit ${$page.url.pathname}`, {
icon: '⛔'
});
goto('/');
return;
}
});
async function getUsers() {
$pocketbase
.collection('users')
.getFullList(1000, { sort: 'username' })
.then((data) => {
users = data as User[];
})
.catch((err) => {
toast.error(err.message);
});
}
async function getPermissions() {
$pocketbase
.collection('permissions')
.getFullList(1000, { expand: 'user' })
.then((data) => {
permissions = data as Permission[];
})
.catch((err) => {
toast.error(err.message);
});
}
async function getDevices() {
$pocketbase
.collection('devices')
.getFullList(1000, { sort: 'name' })
.then((resp) => {
devices = resp as Device[];
})
.catch((err) => {
toast.error(err.message);
});
}
function save() {
let errors = 0;
permissions.forEach(async (permission) => {
if (permission.id === undefined) {
await $pocketbase
.collection('permissions')
.create(permission)
.catch((err) => {
toast.error(err.message);
errors += 1;
});
} else {
await $pocketbase
.collection('permissions')
.update(permission.id, permission)
.catch((err) => {
toast.error(err.message);
errors += 1;
});
}
});
if (errors === 0) {
toast.success('Permissions saved.');
} else {
toast.error(`Failed to save ${errors} ${errors > 1 ? 'users' : 'user'}.`);
}
}
</script>
{#await Promise.all([getUsers(), getPermissions(), getDevices()])}
<PageLoading />
{:then}
<h1 class="text-3xl font-bold mb-8">Permissions</h1>
<form on:submit|preventDefault={save}>
{#each users as user, index}
<div class="card w-full bg-base-300 shadow-xl mt-6">
<div class="card-body gap-4">
<h2 class="card-title">
<label tabindex="-1" class="avatar" for="avatar{index}">
<div class="w-10 rounded-full" id="avatar{index}">
<img
src="{backendUrl}_/images/avatars/avatar{Math.floor(
Math.random() * 9
).toString()}.svg"
alt="Profile pic"
/>
</div>
</label>
{user.username}
</h2>
{#each permissions.filter((perm) => perm.user === user.id) as permission}
<div class="form-control w-fit">
<label class="label cursor-pointer gap-2">
<input
type="checkbox"
bind:checked={permission.create}
class="checkbox checked:checkbox-primary"
/>
<span class="label-text font-bold"
>Allow {user.username} to create new devices and edit device groups?</span
>
</label>
</div>
{/each}
<div class="grid grid-cols-5 gap-4 justify-items-center">
<div class="font-bold" />
<div class="font-bold">Read</div>
<div class="font-bold">Update</div>
<div class="font-bold">Delete</div>
<div class="font-bold">Power</div>
{#each devices as device}
{#each permissions.filter((perm) => perm.user === user.id) as permission}
<div
class="flex flex-row flex-wrap gap-2 place-self-start break-all col-start-1 col-end-6 sm:col-end-1"
>
<span class="font-bold">{device.name}</span>
<span class="badge hidden sm:block">{device.ip}</span>
</div>
<input
type="checkbox"
class="checkbox checked:checkbox-primary col-start-2"
bind:group={permission.read}
value={device.id}
/>
<input
type="checkbox"
class="checkbox checked:checkbox-primary"
bind:group={permission.update}
value={device.id}
/>
<input
type="checkbox"
class="checkbox checked:checkbox-primary"
bind:group={permission.delete}
value={device.id}
/>
<input
type="checkbox"
class="checkbox checked:checkbox-primary"
bind:group={permission.power}
value={device.id}
/>
{/each}
{/each}
</div>
</div>
</div>
{:else}
<div class="container text-center">
<p>No users</p>
</div>
{/each}
{#if users.length > 0}
<div class="card-actions mt-6 justify-end gap-4">
<button class="btn btn-success" type="submit"><Fa icon={faSave} />Save</button>
</div>
{/if}
</form>
{/await}

View File

@@ -0,0 +1,324 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { pocketbase, isAdmin, backendUrl } from '$lib/stores/pocketbase';
import PageLoading from '$lib/components/PageLoading.svelte';
import toast from 'svelte-french-toast';
import Fa from 'svelte-fa';
import { faPlus, faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
import type { User } from '$lib/types/user';
import type { Device } from '$lib/types/device';
import type { Permission } from '$lib/types/permission';
let getUsersPromise = getUsers();
let getPermissionsPromise = getPermissions();
let getDevicesPromise = getDevices();
let deleteModal: HTMLDialogElement;
let users = [] as User[];
let permissions = [] as Permission[];
let devices = [] as Device[];
let newUser = {
username: '',
password: '',
passwordConfirm: ''
};
$: users.map((user) => {
if (!permissions.find((perm) => perm.user === user.id)) {
permissions = [...permissions, { user: user.id } as Permission];
}
});
onMount(() => {
if (!$isAdmin) {
toast(`You don't have permission to visit ${$page.url.pathname}`, {
icon: '⛔'
});
goto('/');
return;
}
});
async function getUsers() {
$pocketbase
.collection('users')
.getFullList(1000, { sort: 'username' })
.then((data) => {
users = data as User[];
})
.catch((err) => {
toast.error(err.message);
});
}
async function createUser() {
await $pocketbase
.collection('users')
.create(newUser)
.then((data) => {
toast.success(`User ${data.username} created.`);
newUser.username = '';
newUser.password = '';
newUser.passwordConfirm = '';
reload();
})
.catch((err) => {
toast.error(err.message);
});
}
async function deleteUser(user: User) {
await $pocketbase
.collection('users')
.delete(user.id)
.then(async () => {
toast.success(`User ${user.username} deleted.`);
let permission = permissions.find((perm) => perm.user === user.id);
if (permission?.id !== undefined) {
await $pocketbase
.collection('permissions')
.delete(permission.id)
.then(() => {
toast.success(`Permissions for ${user.username} deleted.`);
})
.catch((err) => {
toast.error(err.message);
});
}
reload();
})
.catch((err) => {
toast.error(err.message);
});
}
async function getPermissions() {
$pocketbase
.collection('permissions')
.getFullList(1000, { expand: 'user' })
.then((data) => {
permissions = data as Permission[];
})
.catch((err) => {
toast.error(err.message);
});
}
async function getDevices() {
$pocketbase
.collection('devices')
.getFullList(1000, { sort: 'name' })
.then((resp) => {
devices = resp as Device[];
})
.catch((err) => {
toast.error(err.message);
});
}
async function save(user: User) {
let permission = permissions.find((perm) => perm.user === user.id);
if (permission?.id === undefined) {
await $pocketbase
.collection('permissions')
.create(permission)
.then((data) => {
permission = data as Permission;
// const i = permissions.findIndex((perm) => perm.user === user.id);
// if (i > -1) {
// permissions[i] = data as Permission;
// }
toast.success(`Permissions for ${user.username} created.`);
reload();
})
.catch((err) => {
toast.error(err.message);
return;
});
} else {
await $pocketbase
.collection('permissions')
.update(permission.id, permission)
.then(() => {
toast.success(`Permissions for ${user.username} updated.`);
reload();
})
.catch((err) => {
toast.error(err.message);
});
}
}
function reload() {
getUsersPromise = getUsers();
getPermissionsPromise = getPermissions();
getDevicesPromise = getDevices();
}
</script>
{#await Promise.all([getUsersPromise, getPermissionsPromise, getDevicesPromise])}
<PageLoading />
{:then}
<h1 class="text-3xl font-bold mb-8">Users</h1>
{#each users as user, index}
<form on:submit|preventDefault={() => save(user)}>
<div class="card w-full bg-base-300 shadow-xl mt-6">
<div class="card-body gap-4">
<h2 class="card-title">
<label tabindex="-1" class="avatar" for="avatar{index}">
<div class="w-10 rounded-full" id="avatar{index}">
<img
src="{backendUrl}_/images/avatars/avatar{$pocketbase.authStore.model?.avatar}.svg"
alt="Avatar {$pocketbase.authStore.model?.avatar}"
/>
</div>
</label>
{user.username}
</h2>
{#each permissions.filter((perm) => perm.user === user.id) as permission}
<div class="form-control w-fit">
<label class="label cursor-pointer gap-2">
<input
type="checkbox"
bind:checked={permission.create}
class="checkbox checked:checkbox-primary"
/>
<span class="label-text font-bold"
>Allow {user.username} to create new devices and edit device groups?</span
>
</label>
</div>
{/each}
<div class="collapse collapse-arrow bg-base-200">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">Device permissions</div>
<div class="collapse-content">
{#if devices.length === 0}
<p>No devices here. <a href="/device/new" class="link">Create new device</a></p>
{:else}
<div class="grid grid-cols-5 gap-4 justify-items-center">
<div class="font-bold" />
<div class="font-bold">Read</div>
<div class="font-bold">Update</div>
<div class="font-bold">Delete</div>
<div class="font-bold">Power</div>
{#each devices as device}
<hr class="col-span-full w-full border-b-1 opacity-30 border-neutral" />
{#each permissions.filter((perm) => perm.user === user.id) as permission}
<div
class="flex flex-row flex-wrap gap-2 place-self-start break-all col-span-full sm:col-span-1"
>
<span class="font-bold">{device.name}</span>
<span class="badge hidden sm:block">{device.ip}</span>
</div>
<input
type="checkbox"
class="checkbox checked:checkbox-primary col-start-2"
bind:group={permission.read}
value={device.id}
/>
<input
type="checkbox"
class="checkbox checked:checkbox-primary"
bind:group={permission.update}
value={device.id}
/>
<input
type="checkbox"
class="checkbox checked:checkbox-primary"
bind:group={permission.delete}
value={device.id}
/>
<input
type="checkbox"
class="checkbox checked:checkbox-primary"
bind:group={permission.power}
value={device.id}
/>
{/each}
{/each}
</div>
{/if}
</div>
</div>
<div class="justify-end join">
<button
class="join-item btn btn-error"
type="button"
on:click={() => deleteModal.showModal()}><Fa icon={faTrash} />Delete</button
>
<button class="join-item btn btn-success" type="submit"><Fa icon={faSave} />Save</button
>
</div>
</div>
</div>
</form>
<dialog class="modal" bind:this={deleteModal}>
<form method="dialog" class="modal-box">
<h3 class="font-bold text-lg">Confirm delete</h3>
<p class="py-4">Are you sure you want to delete <strong>{user.username}</strong>?</p>
<div class="modal-action">
<button class="btn">Cancle</button>
<button class="btn btn-error" on:click={() => deleteUser(user)}>Delete</button>
</div>
</form>
</dialog>
{/each}
<div class="card w-full bg-base-300 shadow-xl mt-6">
<div class="card-body">
<h2 class="card-title">Create new user</h2>
<form on:submit|preventDefault={createUser}>
<div class="flex flex-row flex-wrap gap-4">
<div class="form-control w-full max-w-xs">
<label class="label" for="username">
<span class="label-text">Username</span>
</label>
<input
id="username"
type="text"
placeholder="Username"
class="input input-bordered w-full max-w-xs"
required
bind:value={newUser.username}
/>
</div>
<div class="form-control w-full max-w-xs">
<label class="label" for="password">
<span class="label-text">Password</span>
</label>
<input
id="password"
type="password"
placeholder="Password"
class="input input-bordered w-full max-w-xs"
minlength="5"
maxlength="72"
required
bind:value={newUser.password}
/>
</div>
<div class="form-control w-full max-w-xs">
<label class="label" for="passwordConfirm">
<span class="label-text">Password confirm</span>
</label>
<input
id="passwordConfirm"
type="password"
placeholder="Password"
class="input input-bordered w-full max-w-xs"
minlength="5"
maxlength="72"
required
bind:value={newUser.passwordConfirm}
/>
</div>
</div>
<div class="card-actions justify-end">
<button type="submit" class="btn btn-success mt-2"><Fa icon={faPlus} />Add</button>
</div>
</form>
</div>
</div>
{/await}