From 722026c938e4268b1f7ec319a9995c877e5e941d Mon Sep 17 00:00:00 2001
From: bytedream <bytedream@protonmail.com>
Date: Fri, 29 Sep 2023 02:08:34 +0200
Subject: [PATCH] add report admin panel

---
 src/lib/components/Input/Input.svelte         |   2 +-
 src/lib/components/Input/Select.svelte        |   6 +-
 src/lib/components/Input/Textarea.svelte      |  10 +-
 src/lib/permissions.ts                        |  12 +-
 src/lib/server/database.ts                    |  23 +-
 src/lib/stores.ts                             |   1 +
 src/routes/admin/+layout.server.ts            |   8 +-
 src/routes/admin/+layout.svelte               |  16 +-
 src/routes/admin/reports/+page.server.ts      |  17 ++
 src/routes/admin/reports/+page.svelte         | 229 ++++++++++++++++++
 src/routes/admin/reports/+server.ts           | 106 ++++++++
 src/routes/report/+server.ts                  |   9 +-
 .../+layout.svelte                            |   0
 .../+page.server.ts                           |   4 +-
 .../+page.svelte                              |   2 +-
 .../{[...url_id] => [...url_hash]}/+server.ts |   2 +-
 .../ReportCompleted.svelte                    |   0
 .../ReportDraft.svelte                        |   2 +-
 .../ReportSubmitted.svelte                    |   0
 19 files changed, 423 insertions(+), 26 deletions(-)
 create mode 100644 src/routes/admin/reports/+page.server.ts
 create mode 100644 src/routes/admin/reports/+page.svelte
 create mode 100644 src/routes/admin/reports/+server.ts
 rename src/routes/report/{[...url_id] => [...url_hash]}/+layout.svelte (100%)
 rename src/routes/report/{[...url_id] => [...url_hash]}/+page.server.ts (90%)
 rename src/routes/report/{[...url_id] => [...url_hash]}/+page.svelte (95%)
 rename src/routes/report/{[...url_id] => [...url_hash]}/+server.ts (80%)
 rename src/routes/report/{[...url_id] => [...url_hash]}/ReportCompleted.svelte (100%)
 rename src/routes/report/{[...url_id] => [...url_hash]}/ReportDraft.svelte (99%)
 rename src/routes/report/{[...url_id] => [...url_hash]}/ReportSubmitted.svelte (100%)

diff --git a/src/lib/components/Input/Input.svelte b/src/lib/components/Input/Input.svelte
index b65d5b0..2764ed3 100644
--- a/src/lib/components/Input/Input.svelte
+++ b/src/lib/components/Input/Input.svelte
@@ -4,7 +4,7 @@
 
 	export let id: string | null = null;
 	export let name: string | null = null;
-	export let type: string;
+	export let type = 'text';
 	export let value: string | null = null;
 	export let placeholder: string | null = null;
 	export let required = false;
diff --git a/src/lib/components/Input/Select.svelte b/src/lib/components/Input/Select.svelte
index 19f9c98..f7c1f1a 100644
--- a/src/lib/components/Input/Select.svelte
+++ b/src/lib/components/Input/Select.svelte
@@ -1,7 +1,9 @@
 <script lang="ts">
-	export let id: string;
+	import { createEventDispatcher } from 'svelte';
+
+	export let id: string | null = null;
 	export let name: string | null = null;
-	export let value: string | null = null;
+	export let value: any | null = null;
 	export let label: string | null = null;
 	export let notice: string | null = null;
 	export let required = false;
diff --git a/src/lib/components/Input/Textarea.svelte b/src/lib/components/Input/Textarea.svelte
index c5583d1..7ee8e14 100644
--- a/src/lib/components/Input/Textarea.svelte
+++ b/src/lib/components/Input/Textarea.svelte
@@ -1,4 +1,6 @@
 <script lang="ts">
+	import { createEventDispatcher } from 'svelte';
+
 	export let id: string | null = null;
 	export let name: string | null = null;
 	export let value: string | null = null;
@@ -6,8 +8,11 @@
 	export let notice: string | null = null;
 	export let required = false;
 	export let disabled = false;
+	export let readonly = false;
 	export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
 	export let rows = 2;
+
+	const dispatch = createEventDispatcher();
 </script>
 
 <div>
@@ -22,17 +27,20 @@
 		</label>
 	{/if}
 	<textarea
-		class="textarea textarea-bordered w-full"
+		class="textarea w-full"
 		class:textarea-xs={size === 'xs'}
 		class:textarea-sm={size === 'sm'}
 		class:textarea-md={size === 'md'}
 		class:textarea-lg={size === 'lg'}
+		class:textarea-bordered={!readonly}
 		{id}
 		{name}
 		{required}
 		{disabled}
+		{readonly}
 		{rows}
 		bind:value
+		on:click={(e) => dispatch('click', e)}
 	/>
 	{#if notice}
 		<label class="label" for={id}>
diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts
index 6e96bb8..d6d4238 100644
--- a/src/lib/permissions.ts
+++ b/src/lib/permissions.ts
@@ -3,6 +3,8 @@ export class Permissions {
 	static readonly AdminWrite = 4;
 	static readonly UserRead = 8;
 	static readonly UserWrite = 16;
+	static readonly ReportRead = 32;
+	static readonly ReportWrite = 64;
 
 	readonly value: number;
 
@@ -29,7 +31,9 @@ export class Permissions {
 			Permissions.AdminRead,
 			Permissions.AdminWrite,
 			Permissions.UserRead,
-			Permissions.UserWrite
+			Permissions.UserWrite,
+			Permissions.ReportRead,
+			Permissions.ReportWrite
 		];
 	}
 
@@ -45,6 +49,12 @@ export class Permissions {
 	userWrite(): boolean {
 		return (this.value & Permissions.UserWrite) != 0;
 	}
+	reportRead(): boolean {
+		return (this.value & Permissions.ReportRead) != 0;
+	}
+	reportWrite(): boolean {
+		return (this.value & Permissions.ReportWrite) != 0;
+	}
 
 	asArray(): number[] {
 		const array = [];
diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts
index 0325a46..4f293c3 100644
--- a/src/lib/server/database.ts
+++ b/src/lib/server/database.ts
@@ -40,26 +40,35 @@ export class User extends Model {
 export class Report extends Model {
 	@Column({ type: DataTypes.STRING, allowNull: false, unique: true })
 	@Index
-	declare url_id: string;
+	declare url_hash: string;
 	@Column({ type: DataTypes.STRING, allowNull: false })
 	declare subject: string;
 	@Column({ type: DataTypes.STRING })
 	declare body: string;
 	@Column({ type: DataTypes.BOOLEAN, allowNull: false })
 	declare draft: boolean;
-	@Column({ type: DataTypes.BOOLEAN, allowNull: false })
-	declare completed: boolean;
+	@Column({ type: DataTypes.ENUM('java', 'bedrock', 'cracked'), allowNull: false })
+	declare status: 'none' | 'review' | 'reviewed';
+	@Column({ type: DataTypes.STRING })
+	declare notice: string;
+	@Column({ type: DataTypes.STRING })
+	declare statement: string;
 	@Column({ type: DataTypes.INTEGER, allowNull: false })
 	@ForeignKey(() => User)
-	declare reporter_user_id: number;
+	declare reporter_id: number;
 	@Column({ type: DataTypes.INTEGER, allowNull: false })
 	@ForeignKey(() => User)
-	declare reported_user_id: number;
+	declare reported_id: number;
+	@Column({ type: DataTypes.INTEGER })
+	@ForeignKey(() => Admin)
+	declare auditor_id: number;
 
-	@BelongsTo(() => User)
+	@BelongsTo(() => User, 'reporter_id')
 	declare reporter: User;
-	@BelongsTo(() => User)
+	@BelongsTo(() => User, 'reported_id')
 	declare reported: User;
+	@BelongsTo(() => Admin, 'auditor_id')
+	declare auditor: Admin;
 }
 
 @Table({ modelName: 'admin', underscored: true })
diff --git a/src/lib/stores.ts b/src/lib/stores.ts
index 67598cd..cdf8bcd 100644
--- a/src/lib/stores.ts
+++ b/src/lib/stores.ts
@@ -4,4 +4,5 @@ import { writable } from 'svelte/store';
 
 export const playAudio: Writable<boolean> = persisted('playAudio', false);
 
+export const reportCount: Writable<number> = writable(0);
 export const adminCount: Writable<number> = writable(0);
diff --git a/src/routes/admin/+layout.server.ts b/src/routes/admin/+layout.server.ts
index fc357e3..878fc49 100644
--- a/src/routes/admin/+layout.server.ts
+++ b/src/routes/admin/+layout.server.ts
@@ -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
 	};
 };
diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte
index 97dc7e9..bab7137 100644
--- a/src/routes/admin/+layout.svelte
+++ b/src/routes/admin/+layout.svelte
@@ -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>
diff --git a/src/routes/admin/reports/+page.server.ts b/src/routes/admin/reports/+page.server.ts
new file mode 100644
index 0000000..9023020
--- /dev/null
+++ b/src/routes/admin/reports/+page.server.ts
@@ -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
+	};
+};
diff --git a/src/routes/admin/reports/+page.svelte b/src/routes/admin/reports/+page.svelte
new file mode 100644
index 0000000..e667846
--- /dev/null
+++ b/src/routes/admin/reports/+page.svelte
@@ -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>
diff --git a/src/routes/admin/reports/+server.ts b/src/routes/admin/reports/+server.ts
new file mode 100644
index 0000000..784d161
--- /dev/null
+++ b/src/routes/admin/reports/+server.ts
@@ -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;
diff --git a/src/routes/report/+server.ts b/src/routes/report/+server.ts
index cc026f3..9bc074f 100644
--- a/src/routes/report/+server.ts
+++ b/src/routes/report/+server.ts
@@ -17,14 +17,15 @@ export const POST = (async ({ request, url }) => {
 		subject: data.reason,
 		body: null,
 		draft: true,
-		url_id: crypto.randomBytes(18).toString('hex'),
+		status: 'none',
+		url_hash: crypto.randomBytes(18).toString('hex'),
 		completed: false,
-		reporter_user_id: reporter.id,
-		reported_user_id: reported.id
+		reporter_id: reporter.id,
+		reported_id: reported.id
 	});
 
 	return new Response(
-		JSON.stringify({ url: `${url.toString().replace(/\/$/, '')}/${report.url_id}` }),
+		JSON.stringify({ url: `${url.toString().replace(/\/$/, '')}/${report.url_hash}` }),
 		{
 			status: 201
 		}
diff --git a/src/routes/report/[...url_id]/+layout.svelte b/src/routes/report/[...url_hash]/+layout.svelte
similarity index 100%
rename from src/routes/report/[...url_id]/+layout.svelte
rename to src/routes/report/[...url_hash]/+layout.svelte
diff --git a/src/routes/report/[...url_id]/+page.server.ts b/src/routes/report/[...url_hash]/+page.server.ts
similarity index 90%
rename from src/routes/report/[...url_id]/+page.server.ts
rename to src/routes/report/[...url_hash]/+page.server.ts
index 22c71f4..139199f 100644
--- a/src/routes/report/[...url_id]/+page.server.ts
+++ b/src/routes/report/[...url_hash]/+page.server.ts
@@ -5,7 +5,7 @@ import { env } from '$env/dynamic/public';
 
 export const load: PageServerLoad = async ({ params }) => {
 	const report = await Report.findOne({
-		where: { url_id: params.url_id },
+		where: { url_hash: params.url_hash },
 		include: [
 			{ model: User, as: 'reporter' },
 			{ model: User, as: 'reported' }
@@ -16,7 +16,7 @@ export const load: PageServerLoad = async ({ params }) => {
 
 	return {
 		draft: report.draft,
-		completed: report.completed,
+		status: report.status,
 		reason: report.subject,
 		reporter: {
 			name: report.reporter.username
diff --git a/src/routes/report/[...url_id]/+page.svelte b/src/routes/report/[...url_hash]/+page.svelte
similarity index 95%
rename from src/routes/report/[...url_id]/+page.svelte
rename to src/routes/report/[...url_hash]/+page.svelte
index dffb35c..6938b40 100644
--- a/src/routes/report/[...url_id]/+page.svelte
+++ b/src/routes/report/[...url_hash]/+page.svelte
@@ -22,7 +22,7 @@
 				on:submit={() => (data.draft = false)}
 			/>
 		</div>
-	{:else if data.completed}
+	{:else if data.status === 'reviewed'}
 		<ReportCompleted />
 	{:else}
 		<div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}>
diff --git a/src/routes/report/[...url_id]/+server.ts b/src/routes/report/[...url_hash]/+server.ts
similarity index 80%
rename from src/routes/report/[...url_id]/+server.ts
rename to src/routes/report/[...url_hash]/+server.ts
index f51e70e..02cb0d9 100644
--- a/src/routes/report/[...url_id]/+server.ts
+++ b/src/routes/report/[...url_hash]/+server.ts
@@ -2,7 +2,7 @@ import type { RequestHandler } from '@sveltejs/kit';
 import { Report } from '$lib/server/database';
 
 export const POST = (async ({ params }) => {
-	const report = await Report.findOne({ where: { url_id: params.url_id } });
+	const report = await Report.findOne({ where: { url_hash: params.url_hash } });
 
 	if (report == null) return new Response(null, { status: 400 });
 
diff --git a/src/routes/report/[...url_id]/ReportCompleted.svelte b/src/routes/report/[...url_hash]/ReportCompleted.svelte
similarity index 100%
rename from src/routes/report/[...url_id]/ReportCompleted.svelte
rename to src/routes/report/[...url_hash]/ReportCompleted.svelte
diff --git a/src/routes/report/[...url_id]/ReportDraft.svelte b/src/routes/report/[...url_hash]/ReportDraft.svelte
similarity index 99%
rename from src/routes/report/[...url_id]/ReportDraft.svelte
rename to src/routes/report/[...url_hash]/ReportDraft.svelte
index e3b8793..9bfdd3f 100644
--- a/src/routes/report/[...url_id]/ReportDraft.svelte
+++ b/src/routes/report/[...url_hash]/ReportDraft.svelte
@@ -9,7 +9,7 @@
 	export let reason: string;
 
 	async function submitReport() {
-		await fetch(`${env.PUBLIC_BASE_PATH}/report/${$page.params.url_id}`, {
+		await fetch(`${env.PUBLIC_BASE_PATH}/report/${$page.params.url_hash}`, {
 			method: 'POST'
 		});
 	}
diff --git a/src/routes/report/[...url_id]/ReportSubmitted.svelte b/src/routes/report/[...url_hash]/ReportSubmitted.svelte
similarity index 100%
rename from src/routes/report/[...url_id]/ReportSubmitted.svelte
rename to src/routes/report/[...url_hash]/ReportSubmitted.svelte