mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-08-03 19:00:01 -04:00
rename permissions to users, add create new user
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -3,4 +3,5 @@ import type { Record } from 'pocketbase';
|
||||
export type User = Record & {
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: number;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
324
frontend/src/routes/users/+page.svelte
Normal file
324
frontend/src/routes/users/+page.svelte
Normal 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}
|
||||
Reference in New Issue
Block a user