add admin user page

This commit is contained in:
bytedream 2023-08-29 04:23:03 +02:00
parent 1fb71fe899
commit 10b1c01d51
8 changed files with 402 additions and 17 deletions

View File

@ -1,7 +1,6 @@
<svelte:options accessors={true} />
<script lang="ts"> <script lang="ts">
import { IconSolid } from 'svelte-heros-v2'; import { IconSolid } from 'svelte-heros-v2';
import { createEventDispatcher } from 'svelte';
export let id: string | null = null; export let id: string | null = null;
export let name: string | null = null; export let name: string | null = null;
@ -14,6 +13,11 @@
export let inputElement: HTMLInputElement | undefined = undefined; export let inputElement: HTMLInputElement | undefined = undefined;
const dispatch = createEventDispatcher();
function input(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
dispatch('input', e);
}
let initialType = type; let initialType = type;
let passwordEyeSize = { let passwordEyeSize = {
@ -38,6 +42,7 @@
{disabled} {disabled}
bind:value bind:value
bind:this={inputElement} bind:this={inputElement}
on:input={input}
/> />
{:else} {:else}
<div> <div>
@ -58,11 +63,12 @@
class:checkbox-sm={type === 'checkbox' && size === 'sm'} class:checkbox-sm={type === 'checkbox' && size === 'sm'}
class:checkbox-md={type === 'checkbox' && size === 'md'} class:checkbox-md={type === 'checkbox' && size === 'md'}
class:checkbox-lg={type === 'checkbox' && size === 'lg'} class:checkbox-lg={type === 'checkbox' && size === 'lg'}
class:input,input-bordered,w-full={type !== 'checkbox'} class:input,w-full={type !== 'checkbox'}
class:input-xs={type !== 'checkbox' && size === 'xs'} class:input-xs={type !== 'checkbox' && size === 'xs'}
class:input-sm={type !== 'checkbox' && size === 'sm'} class:input-sm={type !== 'checkbox' && size === 'sm'}
class:input-md={type !== 'checkbox' && size === 'md'} class:input-md={type !== 'checkbox' && size === 'md'}
class:input-lg={type !== 'checkbox' && size === 'lg'} class:input-lg={type !== 'checkbox' && size === 'lg'}
class:input-bordered={type !== 'checkbox'}
class:pr-11={initialType === 'password'} class:pr-11={initialType === 'password'}
{id} {id}
{name} {name}
@ -77,6 +83,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
value = e.target?.value; value = e.target?.value;
input(e);
}} }}
/> />
{#if initialType === 'password'} {#if initialType === 'password'}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
export let id: string; export let id: string;
export let name: string | null = null; export let name: string | null = null;
export let value: string; export let value: string | null = null;
export let label: string | null = null; export let label: string | null = null;
export let notice: string | null = null; export let notice: string | null = null;
export let required = false; export let required = false;

View File

@ -21,6 +21,12 @@
<div class="flex h-screen"> <div class="flex h-screen">
<div class="h-full w-max"> <div class="h-full w-max">
<ul class="menu p-4 w-fit h-full bg-base-200 text-base-content"> <ul class="menu p-4 w-fit h-full bg-base-200 text-base-content">
<li>
<a href="{env.PUBLIC_BASE_PATH}/admin/users">
<IconOutline name="user-group-outline" />
<span class="ml-1">Registrierte Nutzer</span>
</a>
</li>
<li> <li>
<a href="{env.PUBLIC_BASE_PATH}/admin/admin"> <a href="{env.PUBLIC_BASE_PATH}/admin/admin">
<IconOutline name="users-outline" /> <IconOutline name="users-outline" />

View File

@ -4,19 +4,19 @@ import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions'; import { Permissions } from '$lib/permissions';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageServerLoad = async ({ cookies }) => {
let admins: Admin[] = []; let admins: (typeof Admin.prototype)[] = [];
if (getSession(cookies, { permissions: [Permissions.AdminRead] }) != null) { if (getSession(cookies, { permissions: [Permissions.AdminRead] }) != null) {
admins = await Admin.findAll({ attributes: { exclude: ['password'] } }); admins = await Admin.findAll({ raw: true, attributes: { exclude: ['password'] } });
} }
const session = getSession(cookies); const session = getSession(cookies);
return { return {
admins: admins.map((v) => { admins: admins.map((v) => {
const vv = JSON.parse(JSON.stringify(v)); const vv = JSON.parse(JSON.stringify(v));
vv.permissions = v.permissions.asArray(); vv.permissions = new Permissions(v.permissions as unknown as number).asArray();
return vv; return vv;
}) as (Admin & { [key: string]: any })[], }),
id: session?.userId, id: session?.userId,
permissions: session?.permissions.value || 0 permissions: session?.permissions.value || 0
}; };

View File

@ -104,7 +104,7 @@
<tbody> <tbody>
{#each data.admins as admin, i} {#each data.admins as admin, i}
<tr> <tr>
<td on:mousedown={(e) => resizeTableColumn(e, 5)}>{i}</td> <td on:mousedown={(e) => resizeTableColumn(e, 5)}>{i + 1}</td>
<td on:mousedown={(e) => resizeTableColumn(e, 5)} <td on:mousedown={(e) => resizeTableColumn(e, 5)}
><Input ><Input
type="text" type="text"
@ -158,10 +158,8 @@
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
disabled={!permissions.adminWrite()} disabled={!permissions.adminWrite()}
on:click={() => { on:click={() => {
admin.username = admin.before.username;
admin.password = admin.before.password;
admin.permissions = admin.before.permissions;
admin.edit = false; admin.edit = false;
admin = admin.before;
}} }}
> >
<IconOutline name="no-symbol-outline" width="18" height="18" /> <IconOutline name="no-symbol-outline" width="18" height="18" />
@ -174,11 +172,7 @@
disabled={!permissions.adminWrite()} disabled={!permissions.adminWrite()}
on:click={() => { on:click={() => {
admin.edit = true; admin.edit = true;
admin.before = { admin.before = structuredClone(admin);
username: admin.username,
password: admin.password,
permissions: admin.permissions
};
}} }}
> >
<IconOutline name="pencil-square-outline" width="18" height="18" /> <IconOutline name="pencil-square-outline" width="18" height="18" />

View File

@ -0,0 +1,11 @@
import type { PageServerLoad } from './$types';
import { User } from '$lib/server/database';
import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions';
export const load: PageServerLoad = async ({ cookies }) => {
return {
count:
getSession(cookies, { permissions: [Permissions.UserRead] }) != null ? await User.count() : 0
};
};

View File

@ -0,0 +1,290 @@
<script lang="ts">
import type { PageData } from './$types';
import { IconOutline, IconSolid } from 'svelte-heros-v2';
import Input from '$lib/components/Input/Input.svelte';
import Select from '$lib/components/Input/Select.svelte';
import { env } from '$env/dynamic/public';
import type { User } from '$lib/server/database';
import { buttonTriggeredRequest } from '$lib/components/utils';
import { browser } from '$app/environment';
export let data: PageData;
let headers = [
{
name: 'Vorname',
key: 'firstname',
asc: false
},
{
name: 'Nachname',
key: 'lastname',
asc: false
},
{ name: 'Geburtstag', key: 'birthday', asc: false, sort: (a, b) => a.birthday - b.birthday },
{ name: 'Telefon', key: 'telephone', asc: false, sort: (a, b) => a.telephone - b.telephone },
{
name: 'Username',
key: 'username',
asc: false
},
{
name: 'Minecraft Edition',
key: 'playertype',
asc: false
},
{
name: 'Passwort',
key: 'password',
asc: false
},
{ name: 'UUID', key: 'uuid', asc: false }
];
let ascHeader: (typeof headers)[0] | null = null;
let currentPageUsers: (typeof User.prototype.dataValues)[] = [];
let currentPageUsersRequest: Promise<void> = new Promise((resolve) => resolve());
let usersCache: (typeof User.prototype.dataValues)[][] = [];
let usersPerPage = 50;
let userPage = 0;
let userTableContainerElement: HTMLDivElement;
function fetchPageUsers(page: number) {
if (!browser) return;
if (userTableContainerElement) userTableContainerElement.scrollTop = 0;
if (usersCache[page]) {
currentPageUsers = usersCache[page];
return;
}
// eslint-disable-next-line no-async-promise-executor
currentPageUsersRequest = new Promise(async (resolve, reject) => {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'POST',
body: JSON.stringify({
limit: usersPerPage,
from: usersPerPage * page
})
});
if (response.ok) {
const pageUsers = await response.json();
currentPageUsers = usersCache[page] = pageUsers;
resolve();
} else {
reject(Error());
}
});
}
async function sortUsers(key: string, reverse: boolean) {
const multiplyValue = reverse ? -1 : 1;
currentPageUsers.sort((entryA, entryB) => {
const a = entryA[key];
const b = entryB[key];
switch (typeof a) {
case 'number':
return (a - b) * multiplyValue;
case 'string':
return a.localeCompare(b) * multiplyValue;
default:
return (a - b) * multiplyValue;
}
});
currentPageUsers = currentPageUsers;
}
$: fetchPageUsers(userPage);
async function updateUser(user: typeof User.prototype.dataValues) {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'PATCH',
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error();
}
}
async function deleteUser(id: number) {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'DELETE',
body: JSON.stringify({
id: id
})
});
if (response.ok) {
currentPageUsers.splice(
currentPageUsers.findIndex((v) => v.id == id),
1
);
currentPageUsers = currentPageUsers;
} else {
throw new Error();
}
}
</script>
<div>
<div class="h-[90vh] overflow-scroll" bind:this={userTableContainerElement}>
<table class="table relative">
<thead>
<tr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0">
<th />
{#each headers as header}
<th>
<button
class="flex items-center"
on:click={() => {
sortUsers(header.key, (ascHeader = ascHeader == header ? null : header));
}}
>
{header.name}
<span class="ml-1">
<IconSolid
name={ascHeader === header ? 'chevron-up-solid' : 'chevron-down-solid'}
width="12"
height="12"
/>
</span>
</button>
</th>
{/each}
<th />
</tr>
</thead>
<tbody>
{#key currentPageUsersRequest}
{#await currentPageUsersRequest}
{#each Array(usersPerPage) as _, i}
<tr class="animate-pulse text-transparent">
<td>{i + 1}</td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td><Input type="date" disabled={true} size="sm" /></td>
<td><Input type="tel" disabled={true} size="sm" /></td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td
><Select id="edition" disabled={true} size="sm">
<option value="java">Java Edition</option>
<option value="bedrock">Bedrock Edition</option>
<option value="cracked">Java cracked</option>
</Select></td
>
<td><Input type="text" disabled={true} size="sm" /></td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td
><div class="flex gap-1">
<button class="btn btn-sm btn-square" disabled />
</div></td
>
</tr>
{/each}
{:then _}
{#each currentPageUsers as user, i}
<tr>
<td>{i + 1}</td>
<td>
<Input type="text" bind:value={user.firstname} disabled={!user.edit} size="sm" />
</td>
<td>
<Input type="text" bind:value={user.lastname} disabled={!user.edit} size="sm" />
</td>
<td>
<Input
type="date"
value={new Date(user.birthday).toISOString().split('T')[0]}
on:input={(e) => (user.birthday = e.detail.target.valueAsDate.toISOString())}
disabled={!user.edit}
size="sm"
/>
</td>
<td>
<Input type="tel" bind:value={user.telephone} disabled={!user.edit} size="sm" />
</td>
<td>
<Input type="text" bind:value={user.username} disabled={!user.edit} size="sm" />
</td>
<td>
<Select id="edition" bind:value={user.playertype} disabled={!user.edit} size="sm">
<option value="java">Java Edition</option>
<option value="bedrock">Bedrock Edition</option>
<option value="cracked">Java cracked</option>
</Select>
</td>
<td>
<Input type="text" bind:value={user.password} disabled={!user.edit} size="sm" />
</td>
<td>
<Input
id="uuid"
type="text"
bind:value={user.uuid}
disabled={!user.edit}
size="sm"
/>
</td>
<td>
<div class="flex gap-1">
{#if user.edit}
<button
class="btn btn-sm btn-square"
on:click={async (e) => {
await buttonTriggeredRequest(e, updateUser(user));
user.edit = false;
}}
>
<IconOutline name="check-outline" width="18" height="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={() => {
user.edit = false;
user = user.before;
}}
>
<IconOutline name="no-symbol-outline" width="18" height="18" />
</button>
{:else}
<button
class="btn btn-sm btn-square"
on:click={() => {
user.before = structuredClone(user);
user.edit = true;
}}
>
<IconOutline name="pencil-square-outline" width="18" height="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={(e) => buttonTriggeredRequest(e, deleteUser(user.id))}
>
<IconOutline name="trash-outline" width="18" height="18" />
</button>
{/if}
</div>
</td>
</tr>
{/each}
{/await}
{/key}
</tbody>
</table>
</div>
<div class="flex justify-center w-full mt-4 mb-6">
<div class="join">
{#each Array(Math.ceil(data.count / usersPerPage) || 1) as _, i}
<button
class="join-item btn"
class:btn-active={i === userPage}
on:click={() => {
userPage = i;
}}>{i + 1}</button
>
{/each}
</div>
</div>
</div>

View File

@ -0,0 +1,77 @@
import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions';
import type { RequestHandler } from '@sveltejs/kit';
import { Admin, User } from '$lib/server/database';
export const POST = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.UserRead] }) == null) {
return new Response(null, {
status: 401
});
}
const data = await request.json();
const limit = data['limit'] || 100;
const from = data['from'] || 0;
const users = await User.findAll({ offset: from, limit: limit });
return new Response(JSON.stringify(users));
}) satisfies RequestHandler;
export const PATCH = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.UserWrite] }) == null) {
return new Response(null, {
status: 401
});
}
const data = await request.json();
const id = data['id'] as string | null;
if (id == null) {
return new Response(null, {
status: 400
});
}
const user = await User.findOne({ where: { id: id } });
if (!user) {
return new Response(null, {
status: 400
});
}
if (data['firstname']) user.firstname = data['firstname'];
if (data['lastname']) user.lastname = data['lastname'];
if (data['birthday']) user.birthday = data['birthday'];
if (data['telephone']) user.telephone = data['telephone'];
if (data['username']) user.username = data['username'];
if (data['playertype']) user.playertype = data['playertype'];
if (data['password']) user.password = data['password'];
if (data['uuid']) user.uuid = data['uuid'];
await user.save();
return new Response();
}) satisfies RequestHandler;
export const DELETE = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.UserWrite] }) == null) {
return new Response(null, {
status: 401
});
}
const data = await request.json();
const id = (data['id'] as number) || null;
if (id == null) {
return new Response(null, {
status: 400
});
}
await User.destroy({ where: { id: id } });
return new Response();
}) satisfies RequestHandler;