add option to create a report via the admin panel
All checks were successful
delpoy / build-and-deploy (push) Successful in 47s

This commit is contained in:
bytedream 2023-09-30 01:01:26 +02:00
parent 61ea07d371
commit 3713c7eaba
7 changed files with 353 additions and 29 deletions

View File

@ -0,0 +1,78 @@
<script lang="ts">
export let id: string | null = null;
export let value = '';
export let suggestionRequired = false;
export let searchSuggestionFunc: (
input: string
) => Promise<{ name: string; value: string }[]> = () => Promise.resolve([]);
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
export let label: string | null = null;
export let required = false;
let inputValue: string;
let searchSuggestions: { name: string; value: string }[] = [];
$: if (!suggestionRequired) value = inputValue;
</script>
<div class="relative">
<div>
{#if label}
<label class="label" for={id}>
<span class="label-text">
{label}
{#if required}
<span class="text-red-700">*</span>
{/if}
</span>
</label>
{/if}
<input
type="search"
class="input input-bordered w-full"
class:input-xs={size === 'xs'}
class:input-sm={size === 'sm'}
class:input-md={size === 'md'}
class:input-lg={size === 'md'}
{id}
{required}
bind:value={inputValue}
on:input={() => {
value = '';
searchSuggestionFunc(inputValue).then((v) => {
searchSuggestions = v;
let searchSuggestionValue = v.find((v) => v.name === inputValue);
if (searchSuggestionValue !== undefined) {
value = searchSuggestionValue.value;
searchSuggestions = [];
}
});
}}
pattern={suggestionRequired ? `${value ? inputValue : 'a^'}` : null}
/>
</div>
{#if inputValue && searchSuggestions.length !== 0}
<ul class="absolute bg-base-200 w-full z-20 menu menu-sm rounded-box">
{#each searchSuggestions as searchSuggestion}
<li class="w-full text-left">
<button
class="block w-full overflow-hidden text-ellipsis whitespace-nowrap"
title="{searchSuggestion.name} ({searchSuggestion.value})"
on:click|preventDefault={() => {
inputValue = searchSuggestion.name;
value = searchSuggestion.value;
searchSuggestions = [];
}}>{searchSuggestion.name}</button
>
</li>
{/each}
</ul>
{/if}
</div>
{#if inputValue && searchSuggestions.length !== 0}
<button
class="absolute top-0 left-0 z-10 w-full h-full cursor-default"
on:click={() => (searchSuggestions = [])}
/>
{/if}

View File

@ -8,6 +8,9 @@
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
import Textarea from '$lib/components/Input/Textarea.svelte'; import Textarea from '$lib/components/Input/Textarea.svelte';
import { reportCount } from '$lib/stores'; import { reportCount } from '$lib/stores';
import HeaderBar from './HeaderBar.svelte';
import { IconOutline } from 'svelte-heros-v2';
import NewReportModal from './NewReportModal.svelte';
export let data: PageData; export let data: PageData;
@ -47,29 +50,12 @@
} }
let saveActiveReportChangesModal: HTMLDialogElement; let saveActiveReportChangesModal: HTMLDialogElement;
let newReportModal: HTMLDialogElement;
</script> </script>
<div class="h-screen flex flex-row"> <div class="h-screen flex flex-row">
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<form class="flex flex-row justify-center space-x-4 mx-4 my-2"> <HeaderBar bind:reportFilter />
<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" /> <hr class="divider my-1 mx-8 border-none" />
<table class="table table-fixed h-fit"> <table class="table table-fixed h-fit">
<colgroup> <colgroup>
@ -124,6 +110,16 @@
<td>{report.draft ? 'Entwurf' : 'Erstellt'}</td> <td>{report.draft ? 'Entwurf' : 'Erstellt'}</td>
</tr> </tr>
{/each} {/each}
<tr>
<td colspan="100">
<div class="flex justify-center items-center">
<button class="btn btn-sm" on:click={() => newReportModal.show()}>
<IconOutline name="plus-outline" />
<span>Neuer Report</span>
</button>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -162,7 +158,7 @@
> >
<Textarea <Textarea
label="Interne Notizen" label="Interne Notizen"
readonly={activeReport.auditor === null && activeReport.notice === null} readonly={activeReport.status === 'none'}
rows={1} rows={1}
bind:value={activeReport.notice} bind:value={activeReport.notice}
/> />
@ -175,13 +171,15 @@
> >
<Textarea <Textarea
label="(Öffentliche) Report Antwort" label="(Öffentliche) Report Antwort"
readonly={activeReport.auditor === null && activeReport.notice === null} readonly={activeReport.status === 'none'}
rows={3} rows={3}
bind:value={activeReport.statement} bind:value={activeReport.statement}
/> />
</div> </div>
<Select label="Bearbeitungsstatus" size="sm" bind:value={activeReport.status}> <Select label="Bearbeitungsstatus" size="sm" bind:value={activeReport.status}>
<option value="none" disabled={activeReport.auditor != null || activeReport.notice} <option
value="none"
disabled={activeReport.auditor != null || activeReport.notice || activeReport.statement}
>Unbearbeitet</option >Unbearbeitet</option
> >
<option value="review">In Bearbeitung</option> <option value="review">In Bearbeitung</option>
@ -227,3 +225,13 @@
<button>close</button> <button>close</button>
</form> </form>
</dialog> </dialog>
<dialog class="modal" bind:this={newReportModal}>
<NewReportModal
on:submit={(e) => {
if (!e.detail.draft) $reportCount += 1;
currentPageReports = [e.detail, ...currentPageReports];
activeReport = currentPageReports[0];
}}
/>
</dialog>

View File

@ -5,6 +5,7 @@ import { Admin, Report, User } from '$lib/server/database';
import type { Attributes } from 'sequelize'; import type { Attributes } from 'sequelize';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import crypto from 'crypto';
export const POST = (async ({ request, cookies }) => { export const POST = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.ReportRead] }) == null) { if (getSession(cookies, { permissions: [Permissions.ReportRead] }) == null) {
@ -104,3 +105,49 @@ export const PATCH = (async ({ request, cookies }) => {
return new Response(); return new Response();
}) satisfies RequestHandler; }) satisfies RequestHandler;
export const PUT = (async ({ request, cookies, url }) => {
if (getSession(cookies, { permissions: [Permissions.ReportWrite] }) == null) {
return new Response(null, {
status: 401
});
}
const data: {
reporter: string;
reported: string;
reason: string;
body: string | null;
} = await request.json();
if (
data.reporter == null ||
data.reported == null ||
data.reason == null ||
data.body === undefined
)
return new Response(null, { status: 400 });
const reporter = await User.findOne({ where: { uuid: data.reporter } });
const reported = await User.findOne({ where: { uuid: data.reported } });
if (reporter == null || reported == null) return new Response(null, { status: 400 });
const report = await Report.create({
subject: data.reason,
body: data.body,
draft: data.body === null,
status: 'none',
url_hash: crypto.randomBytes(18).toString('hex'),
completed: false,
reporter_id: reporter.id,
reported_id: reported.id
});
report.dataValues.reporter = await User.findOne({ where: { id: report.reporter_id } });
report.dataValues.reported = await User.findOne({ where: { id: report.reported_id } });
report.dataValues.auditor = null;
return new Response(JSON.stringify(report), {
status: 201
});
}) satisfies RequestHandler;

View File

@ -0,0 +1,33 @@
<script lang="ts">
import Select from '$lib/components/Input/Select.svelte';
import Input from '$lib/components/Input/Input.svelte';
export let reportFilter = {
reporter: null,
reported: null,
status: null,
draft: false
};
</script>
<div class="flex flex-row justify-center items-end">
<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>
</div>

View File

@ -0,0 +1,137 @@
<script lang="ts">
import Input from '$lib/components/Input/Input.svelte';
import { env } from '$env/dynamic/public';
import Textarea from '$lib/components/Input/Textarea.svelte';
import Search from '$lib/components/Input/Search.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let reporter: string;
let reported: string;
let reason = '';
let body = '';
async function usernameSuggestions(username: string): Promise<{ name: string; value: string }[]> {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'POST',
body: JSON.stringify({
limit: 6,
search: username,
slim: true
})
});
const json: { username: string; uuid: string }[] = await response.json();
return json.map((v) => {
return { name: v.username, value: v.uuid };
});
}
async function newReport() {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
method: 'PUT',
body: JSON.stringify({
reporter: reporter,
reported: reported,
reason: reason,
body: body || null
})
});
if (response.ok) dispatch('submit', await response.json());
}
let globalCloseForm: HTMLFormElement;
let reportForm: HTMLFormElement;
let confirmDialog: HTMLDialogElement;
</script>
<form method="dialog" class="modal-box" bind:this={reportForm}>
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
on:click|preventDefault={() => globalCloseForm.submit()}>✕</button
>
<h3 class="font-roboto text-xl">Neuer Report</h3>
<div class="space-y-2 mt-2 px-1 max-h-[70vh] overflow-y-scroll">
<div>
<Search
size="sm"
suggestionRequired={true}
searchSuggestionFunc={usernameSuggestions}
label="Reporter"
required={true}
bind:value={reporter}
/>
<Search
size="sm"
suggestionRequired={true}
searchSuggestionFunc={usernameSuggestions}
label="Reporteter User"
required={true}
bind:value={reported}
/>
</div>
<div class="divider mx-4 pt-3" />
<Input type="text" bind:value={reason} required={true} pickyWidth={false}>
<span slot="label">Report Grund</span>
</Input>
<div>
<Textarea rows={4} label="Details über den Report Grund" bind:value={body} />
</div>
</div>
<div class="flex flex-row space-x-2 mt-6">
<Input
type="submit"
value="Erstellen"
on:click={(e) => {
if (reportForm.checkValidity()) {
e.detail.preventDefault();
confirmDialog.show();
}
}}
/>
<Input
type="submit"
value="Abbrechen"
on:click={(e) => {
e.detail.preventDefault();
globalCloseForm.submit();
}}
/>
</div>
</form>
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]" bind:this={globalCloseForm}>
<button>close</button>
</form>
<dialog class="modal" bind:this={confirmDialog}>
<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 mb-2">Report Erstellen?</h3>
{#if body}
<p>
Dadurch, dass bereits Details über den Report Grund hinzugefügt wurden, ist es nach dem
Erstellen nicht mehr möglich, den Report Inhalt zu ändern
</p>
{:else}
<p>
Der Report wird als Entwurf gespeichert und kann nach dem Erstellen über den Report-Link
bearbeitet werden
</p>
{/if}
<div class="flex flex-row space-x-2 mt-6">
<Input
type="submit"
value="Erstellen"
on:click={async () => {
await newReport();
globalCloseForm.submit();
}}
/>
<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

@ -1,7 +1,8 @@
import { getSession } from '$lib/server/session'; import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions'; import { Permissions } from '$lib/permissions';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { Admin, User } from '$lib/server/database'; import { User } from '$lib/server/database';
import { type Attributes, Op } from 'sequelize';
export const POST = (async ({ request, cookies }) => { export const POST = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.UserRead] }) == null) { if (getSession(cookies, { permissions: [Permissions.UserRead] }) == null) {
@ -10,11 +11,28 @@ export const POST = (async ({ request, cookies }) => {
}); });
} }
const data = await request.json(); const data: {
const limit = data['limit'] || 100; limit: number | null;
const from = data['from'] || 0; from: number | null;
search: string | null;
slim: boolean | null;
} = await request.json();
const users = await User.findAll({ offset: from, limit: limit }); const usersFindOptions: Attributes<User> = {};
if (data.search) {
Object.assign(usersFindOptions, {
[Op.or]: {
username: { [Op.like]: `%${data.search}%` },
uuid: { [Op.like]: `%${data.search}%` }
}
});
}
const users = await User.findAll({
where: usersFindOptions,
attributes: data.slim ? ['username', 'uuid'] : undefined,
offset: data.from || 0,
limit: data.limit || 100
});
return new Response(JSON.stringify(users)); return new Response(JSON.stringify(users));
}) satisfies RequestHandler; }) satisfies RequestHandler;

View File

@ -1,11 +1,12 @@
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { Report, User } from '$lib/server/database'; import { Report, User } from '$lib/server/database';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { env } from '$env/dynamic/public';
export const POST = (async ({ request, url }) => { export const POST = (async ({ request, url }) => {
const data: { reporter: string; reported: string; reason: string } = await request.json(); const data: { reporter: string; reported: string; reason: string } = await request.json();
if (data.reporter == undefined || data.reported == undefined || data.reason == undefined) if (data.reporter == null || data.reported == null || data.reason == null)
return new Response(null, { status: 400 }); return new Response(null, { status: 400 });
const reporter = await User.findOne({ where: { uuid: data.reporter } }); const reporter = await User.findOne({ where: { uuid: data.reporter } });
@ -25,7 +26,9 @@ export const POST = (async ({ request, url }) => {
}); });
return new Response( return new Response(
JSON.stringify({ url: `${url.toString().replace(/\/$/, '')}/${report.url_hash}` }), JSON.stringify({
url: `${url.protocol}//${url.host}${env.PUBLIC_BASE_PATH || ''}/report/${report.url_hash}`
}),
{ {
status: 201 status: 201
} }