add report admin panel
All checks were successful
delpoy / build-and-deploy (push) Successful in 53s

This commit is contained in:
2023-09-29 02:08:34 +02:00
parent 37c230575d
commit 722026c938
19 changed files with 423 additions and 26 deletions

View File

@ -1,5 +1,5 @@
import type { LayoutServerLoad } from './$types';
import { Admin, User } from '$lib/server/database';
import { Admin, Report, User } from '$lib/server/database';
import { getSession } from '$lib/server/session';
import { redirect } from '@sveltejs/kit';
import { env } from '$env/dynamic/public';
@ -12,6 +12,10 @@ export const load: LayoutServerLoad = async ({ route, cookies }) => {
return {
userCount: session?.permissions.userRead() ? await User.count() : null,
adminCount: session?.permissions.adminRead() ? await Admin.count() : null
reportCount: session?.permissions.reportRead()
? await Report.count({ where: { draft: false, status: ['none', 'review'] } })
: null,
adminCount: session?.permissions.adminRead() ? await Admin.count() : null,
self: session ? await Admin.findOne({ where: { id: session.userId } }) : null
};
};

View File

@ -5,7 +5,7 @@
import { buttonTriggeredRequest } from '$lib/components/utils';
import { goto } from '$app/navigation';
import type { LayoutData } from './$types';
import { adminCount } from '$lib/stores';
import { adminCount, reportCount } from '$lib/stores';
async function logout() {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/logout`, {
@ -19,11 +19,12 @@
}
export let data: LayoutData;
if (data.reportCount) $reportCount = data.reportCount;
if (data.adminCount) $adminCount = data.adminCount;
</script>
{#if $page.url.pathname !== `${env.PUBLIC_BASE_PATH}/admin/login`}
<div class="flex h-screen">
<div class="flex h-screen w-screen">
<div class="h-full">
<ul class="menu p-4 w-max h-full bg-base-200 text-base-content">
{#if data.userCount != null}
@ -35,6 +36,15 @@
</a>
</li>
{/if}
{#if data.reportCount != null}
<li>
<a href="{env.PUBLIC_BASE_PATH}/admin/reports">
<IconOutline name="flag-outline" />
<span class="ml-1">Reports</span>
<div class="badge">{$reportCount}</div>
</a>
</li>
{/if}
{#if data.adminCount != null}
<li>
<a href="{env.PUBLIC_BASE_PATH}/admin/admin">
@ -52,7 +62,7 @@
</li>
</ul>
</div>
<div class="h-full w-full overflow-scroll">
<div class="h-full w-full overflow-y-scroll overflow-x-hidden">
<slot />
</div>
</div>

View File

@ -0,0 +1,17 @@
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { env } from '$env/dynamic/public';
import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions';
export const load: PageServerLoad = async ({ parent, cookies }) => {
const { reportCount } = await parent();
if (reportCount == null) throw redirect(302, `${env.PUBLIC_BASE_PATH}/admin`);
const { self } = await parent();
return {
count: getSession(cookies, { permissions: [Permissions.UserRead] }) != null ? reportCount : 0,
self: self
};
};

View File

@ -0,0 +1,229 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import type { Report } from '$lib/server/database';
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import Select from '$lib/components/Input/Select.svelte';
import Input from '$lib/components/Input/Input.svelte';
import Textarea from '$lib/components/Input/Textarea.svelte';
import { reportCount } from '$lib/stores';
export let data: PageData;
let currentPageReports: (typeof Report.prototype.dataValues)[] = [];
let reportsPerPage = 50;
let reportPage = 0;
let reportFilter = { draft: false, status: null, reporter: null, reported: null };
let activeReport: typeof Report.prototype.dataValues | null = null;
async function fetchPageReports(
page: number,
filter: any
): Promise<(typeof Report.prototype.dataValues)[]> {
if (!browser) return [];
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
method: 'POST',
body: JSON.stringify({ ...filter, limit: reportsPerPage, from: reportPage * page })
});
return await response.json();
}
$: fetchPageReports(reportPage, reportFilter).then((r) => (currentPageReports = r));
async function updateActiveReport() {
await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
method: 'PATCH',
body: JSON.stringify({
id: activeReport.id,
auditor: data.self?.id || -1,
notice: activeReport.notice || '',
statement: activeReport.statement || '',
status: activeReport.status
})
});
}
let saveActiveReportChangesModal: HTMLDialogElement;
</script>
<div class="h-screen flex flex-row">
<div class="w-full flex flex-col">
<form class="flex flex-row justify-center space-x-4 mx-4 my-2">
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reporter}>
<span slot="label">Report Ersteller</span>
</Input>
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reported}>
<span slot="label">Reportete Spieler</span>
</Input>
<Select label="Bearbeitungsstatus" size="sm" bind:value={reportFilter.status}>
<option value="none">Unbearbeitet</option>
<option value="review">In Bearbeitung</option>
<option value={null}>Unbearbeitet & In Bearbeitung</option>
<option value="reviewed">Bearbeitet</option>
</Select>
<Select label="Reportstatus" size="sm" bind:value={reportFilter.draft}>
<option value={false}>Erstellt</option>
<option value={true}>Entwurf</option>
<option value={null}>Erstellt & Entwurf</option>
</Select>
</form>
<hr class="divider my-1 mx-8 border-none" />
<table class="table table-fixed h-fit">
<colgroup>
<col style="width: 20%" />
<col style="width: 15%" />
<col style="width: 15%" />
<col style="width: 20%" />
<col style="width: 15%" />
<col style="width: 15%" />
</colgroup>
<thead>
<tr>
<th>Grund</th>
<th>Ersteller</th>
<th>Reporteter User</th>
<th>Datum</th>
<th>Bearbeitungsstatus</th>
<th>Reportstatus</th>
</tr>
</thead>
<tbody>
{#each currentPageReports as report}
<tr
class="hover [&>*]:text-sm cursor-pointer"
class:bg-base-200={activeReport === report}
on:click={() => {
activeReport = report;
activeReport.originalStatus = report.status;
}}
>
<td title={report.subject}><div class="overflow-scroll">{report.subject}</div></td>
<td>{report.reporter.username}</td>
<td>{report.reported.username}</td>
<td
>{new Intl.DateTimeFormat('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(report.createdAt))} Uhr</td
>
<td>
{report.status === 'none'
? 'Unbearbeitet'
: report.status === 'review'
? 'In Bearbeitung'
: report.status === 'reviewed'
? 'Bearbeitet'
: ''}
</td>
<td>{report.draft ? 'Entwurf' : 'Erstellt'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if activeReport}
<div
class="relative flex flex-col w-2/5 bg-base-200/50 px-4 py-6 overflow-scroll"
transition:fly={{ x: 200, duration: 200 }}
>
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
on:click={() => (activeReport = null)}>✕</button
>
<h3 class="font-roboto font-semibold text-2xl">Report</h3>
<div class="break-words my-2">
<i class="font-medium">{activeReport.reporter.username}</i> hat
<i class="font-medium">{activeReport.reported.username}</i>
am {new Intl.DateTimeFormat('de-DE', {
year: 'numeric',
month: 'long',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(activeReport.createdAt))} Uhr reportet.
</div>
<div class="w-full">
<Textarea readonly={true} rows={1} label="Report Grund" value={activeReport.subject} />
<Textarea readonly={true} rows={4} label="Report Details" value={activeReport.body} />
</div>
<div class="divider mx-4" />
<div>
<div
class="w-full"
title={activeReport.status === 'none'
? 'Zum Bearbeiten den Bearbeitungsstatus ändern'
: ''}
>
<Textarea
label="Interne Notizen"
readonly={activeReport.auditor === null && activeReport.notice === null}
rows={1}
bind:value={activeReport.notice}
/>
</div>
<div
class="w-full"
title={activeReport.status === 'none'
? 'Zum Bearbeiten den Bearbeitungsstatus ändern'
: ''}
>
<Textarea
label="(Öffentliche) Report Antwort"
readonly={activeReport.auditor === null && activeReport.notice === null}
rows={3}
bind:value={activeReport.statement}
/>
</div>
<Select label="Bearbeitungsstatus" size="sm" bind:value={activeReport.status}>
<option value="none" disabled={activeReport.auditor != null || activeReport.notice}
>Unbearbeitet</option
>
<option value="review">In Bearbeitung</option>
<option value="reviewed">Bearbeitet</option>
</Select>
</div>
<div class="self-end mt-auto pt-6 w-full flex justify-center">
<Input
type="submit"
value="Speichern"
on:click={() => saveActiveReportChangesModal.show()}
/>
</div>
</div>
{/if}
</div>
<dialog class="modal" bind:this={saveActiveReportChangesModal}>
<form method="dialog" class="modal-box">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<h3 class="font-roboto text-xl">Änderungen Speichern?</h3>
<div class="flex flex-row space-x-2 mt-6">
<Input
type="submit"
value="Speichern"
on:click={async () => {
await updateActiveReport();
currentPageReports = [...currentPageReports];
if (activeReport.originalStatus !== 'reviewed' && activeReport.status === 'reviewed') {
$reportCount -= 1;
} else if (
activeReport.originalStatus === 'reviewed' &&
activeReport.status !== 'reviewed'
) {
$reportCount += 1;
}
}}
/>
<Input type="submit" value="Abbrechen" />
</div>
</form>
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
<button>close</button>
</form>
</dialog>

View File

@ -0,0 +1,106 @@
import type { RequestHandler } from '@sveltejs/kit';
import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions';
import { Admin, Report, User } from '$lib/server/database';
import type { Attributes } from 'sequelize';
import { Op } from 'sequelize';
import { env } from '$env/dynamic/private';
export const POST = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.ReportRead] }) == null) {
return new Response(null, {
status: 401
});
}
const data: {
limit: number | null;
from: number | null;
draft: boolean | null;
status: 'none' | 'review' | 'reviewed' | null;
reporter: string | null;
reported: string | null;
} = await request.json();
const reportFindOptions: Attributes<Report> = {};
if (data.draft != null) reportFindOptions.draft = data.draft;
reportFindOptions.status = data.status == null ? ['none', 'review'] : data.status;
if (data.reporter != null) {
const reporter_ids = await User.findAll({
attributes: ['id'],
where: { username: { [Op.like]: `%${data.reporter}%` } }
});
reportFindOptions.reporter_id = reporter_ids.map((u) => u.id);
}
if (data.reported != null) {
const reported_ids = await User.findAll({
attributes: ['id'],
where: { username: { [Op.like]: `%${data.reported}%` } }
});
reportFindOptions.reported_id = reported_ids.map((u) => u.id);
}
let reports = await Report.findAll({
where: reportFindOptions,
include: [
{ model: User, as: 'reporter' },
{ model: User, as: 'reported' },
{ model: Admin, as: 'auditor' }
],
order: ['created_at'],
offset: data.from || 0,
limit: data.limit || 100
});
reports = reports.map((r) => {
if (r.auditor_id === null && r.status != 'none') {
// if the report was edited by the admin account set by the env variable, it has no relation to the admin
// table in the database, so it gets manually created here. we just assume that the auditor_id is never null
// when not edited by the env admin account
r.auditor_id = -1;
r.dataValues.auditor = {
id: -1,
username: env.ADMIN_USER,
permissions: new Permissions(Permissions.allPermissions()),
createdAt: 0,
updatedAt: 0
};
} else if (r.auditor) {
delete r.dataValues.auditor.password;
}
return r;
});
return new Response(JSON.stringify(reports));
}) satisfies RequestHandler;
export const PATCH = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.ReportWrite] }) == null) {
return new Response(null, {
status: 401
});
}
const data: {
id: number;
auditor: number;
notice: string | null;
statement: string | null;
status: 'none' | 'review' | 'reviewed' | null;
} = await request.json();
if (data.id === null || data.auditor === null) return new Response(null, { status: 400 });
const report = await Report.findOne({ where: { id: data.id } });
const admin = await Admin.findOne({ where: { id: data.auditor } });
if (report === null || (admin === null && data.auditor != -1))
return new Response(null, { status: 400 });
if (data.notice != null) report.notice = data.notice;
if (data.statement != null) report.statement = data.statement;
if (data.status != null) report.status = data.status;
if (admin != null) report.auditor_id = admin.id;
await report.save();
return new Response();
}) satisfies RequestHandler;