show report attachments in admin ui
This commit is contained in:
@ -66,8 +66,20 @@ export const report = {
|
|||||||
const hash = md5Hash.digest('hex');
|
const hash = md5Hash.digest('hex');
|
||||||
const filePath = path.join(UPLOAD_PATH!, hash);
|
const filePath = path.join(UPLOAD_PATH!, hash);
|
||||||
|
|
||||||
|
let type: 'image' | 'video';
|
||||||
|
if (allowedImageTypes.includes(file.type)) {
|
||||||
|
type = 'image';
|
||||||
|
} else if (allowedVideoTypes.includes(file.type)) {
|
||||||
|
type = 'video';
|
||||||
|
} else {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Invalid file type'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await tx.addReportAttachment({
|
await tx.addReportAttachment({
|
||||||
type: 'video',
|
type: type,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
reportId: report.id
|
reportId: report.id
|
||||||
});
|
});
|
||||||
@ -198,6 +210,18 @@ export const report = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
reportAttachments: defineAction({
|
||||||
|
input: z.object({
|
||||||
|
reportId: z.number()
|
||||||
|
}),
|
||||||
|
handler: async (input, context) => {
|
||||||
|
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reportAttachments: (await db.getReportAttachments(input)) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
addStrikeReason: defineAction({
|
addStrikeReason: defineAction({
|
||||||
input: z.object({
|
input: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { editReport, type Report, type ReportStatus, type StrikeReasons } from './reports.ts';
|
import { editReport, getReportAttachments, type Report, type ReportStatus, type StrikeReasons } from './reports.ts';
|
||||||
import Input from '@components/input/Input.svelte';
|
import Input from '@components/input/Input.svelte';
|
||||||
import Textarea from '@components/input/Textarea.svelte';
|
import Textarea from '@components/input/Textarea.svelte';
|
||||||
import Select from '@components/input/Select.svelte';
|
import Select from '@components/input/Select.svelte';
|
||||||
@ -7,6 +7,9 @@
|
|||||||
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
|
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
|
||||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||||
|
|
||||||
|
// html bindings
|
||||||
|
let previewDialogElem: HTMLDialogElement;
|
||||||
|
|
||||||
// types
|
// types
|
||||||
interface Props {
|
interface Props {
|
||||||
strikeReasons: StrikeReasons;
|
strikeReasons: StrikeReasons;
|
||||||
@ -18,11 +21,15 @@
|
|||||||
|
|
||||||
// states
|
// states
|
||||||
let reportedTeam = $state<{ id: number; name: string } | null>(report?.reported ?? null);
|
let reportedTeam = $state<{ id: number; name: string } | null>(report?.reported ?? null);
|
||||||
|
|
||||||
let status = $state<'open' | 'closed' | null>(null);
|
let status = $state<'open' | 'closed' | null>(null);
|
||||||
let notice = $state<string | null>(null);
|
let notice = $state<string | null>(null);
|
||||||
let statement = $state<string | null>(null);
|
let statement = $state<string | null>(null);
|
||||||
let strikeReason = $state<string | null>(String(report?.strike?.strikeReasonId ?? null));
|
let strikeReason = $state<string | null>(String(report?.strike?.strikeReasonId ?? null));
|
||||||
|
|
||||||
|
let reportAttachments = $state<{ type: 'image' | 'video'; hash: string }[]>([]);
|
||||||
|
let previewReportAttachment = $state<{ type: 'image' | 'video'; hash: string } | null>(null);
|
||||||
|
|
||||||
// consts
|
// consts
|
||||||
const strikeReasonValues = strikeReasons.reduce(
|
const strikeReasonValues = strikeReasons.reduce(
|
||||||
(prev, curr) => Object.assign(prev, { [curr.id]: `${curr.name} (${curr.weight})` }),
|
(prev, curr) => Object.assign(prev, { [curr.id]: `${curr.name} (${curr.weight})` }),
|
||||||
@ -40,6 +47,14 @@
|
|||||||
notice = reportStatus.notice;
|
notice = reportStatus.notice;
|
||||||
statement = reportStatus.statement;
|
statement = reportStatus.statement;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getReportAttachments(report).then((value) => {
|
||||||
|
if (value) reportAttachments = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (previewReportAttachment) previewDialogElem.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// callbacks
|
// callbacks
|
||||||
@ -71,16 +86,38 @@
|
|||||||
<div class="w-[34rem]">
|
<div class="w-[34rem]">
|
||||||
<TeamSearch value={report?.reporter.name} label="Report Team" readonly mustMatch />
|
<TeamSearch value={report?.reporter.name} label="Report Team" readonly mustMatch />
|
||||||
<TeamSearch value={report?.reported?.name} label="Reportetes Team" onSubmit={(team) => (reportedTeam = team)} />
|
<TeamSearch value={report?.reported?.name} label="Reportetes Team" onSubmit={(team) => (reportedTeam = team)} />
|
||||||
<Textarea bind:value={notice} label="Interne Notizen" rows={8} />
|
<Textarea bind:value={notice} label="Interne Notizen" rows={10} />
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Input value={report?.reason} label="Grund" readonly dynamicWidth />
|
<Input value={report?.reason} label="Grund" readonly dynamicWidth />
|
||||||
<Textarea value={report?.body} label="Inhalt" readonly dynamicWidth rows={12} />
|
<Textarea value={report?.body} label="Inhalt" readonly dynamicWidth rows={9} />
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Anhänge</legend>
|
||||||
|
<div class="h-16.5 rounded border border-dashed flex">
|
||||||
|
{#each reportAttachments as reportAttachment (reportAttachment.hash)}
|
||||||
|
<div>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div class="cursor-zoom-in" onclick={() => (previewReportAttachment = reportAttachment)}>
|
||||||
|
{#if reportAttachment.type === 'image'}
|
||||||
|
<img
|
||||||
|
src={location.pathname + '/attachment/' + reportAttachment.hash}
|
||||||
|
alt={reportAttachment.hash}
|
||||||
|
class="w-16 h-16"
|
||||||
|
/>
|
||||||
|
{:else if reportAttachment.type === 'video'}
|
||||||
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video src={location.pathname + '/attachment/' + reportAttachment.hash} class="w-16 h-16"></video>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-horizontal"></div>
|
||||||
<div class="flex flex-col w-[42rem]">
|
<div class="flex flex-col w-[42rem]">
|
||||||
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={5} />
|
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={7} />
|
||||||
<Select
|
<Select
|
||||||
bind:value={status}
|
bind:value={status}
|
||||||
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
|
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
|
||||||
@ -93,3 +130,22 @@
|
|||||||
<button class="btn mt-auto" onclick={onSaveButtonClick}>Speichern</button>
|
<button class="btn mt-auto" onclick={onSaveButtonClick}>Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
class="modal"
|
||||||
|
bind:this={previewDialogElem}
|
||||||
|
onclose={() => setTimeout(() => (previewReportAttachment = null), 300)}
|
||||||
|
>
|
||||||
|
<div class="modal-box">
|
||||||
|
{#if previewReportAttachment?.type === 'image'}
|
||||||
|
<img src={location.pathname + '/attachment/' + previewReportAttachment.hash} alt={previewReportAttachment.hash} />
|
||||||
|
{:else if previewReportAttachment?.type === 'video'}
|
||||||
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video src={location.pathname + '/attachment/' + previewReportAttachment.hash} controls></video>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
||||||
|
<button class="absolute top-3 right-3 btn btn-circle">✕</button>
|
||||||
|
<button class="!cursor-default">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
@ -84,6 +84,18 @@ export async function editReportStatus(report: Report, reportStatus: ReportStatu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getReportAttachments(report: Report) {
|
||||||
|
const { data, error } = await actions.report.reportAttachments({
|
||||||
|
reportId: report.id
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
actionErrorPopup(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.reportAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStrikeReasons() {
|
export async function getStrikeReasons() {
|
||||||
const { data, error } = await actions.report.strikeReasons();
|
const { data, error } = await actions.report.strikeReasons();
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
import { allowedImageTypes, allowedVideoTypes } from '@util/media.ts';
|
import { allowedImageTypes, allowedVideoTypes } from '@util/media.ts';
|
||||||
|
|
||||||
// bindings
|
// bindings
|
||||||
let containerElem: HTMLDivElement;
|
|
||||||
let hiddenFileInputElem: HTMLInputElement;
|
let hiddenFileInputElem: HTMLInputElement;
|
||||||
let previewDialogElem: HTMLDialogElement;
|
let previewDialogElem: HTMLDialogElement;
|
||||||
|
|
||||||
@ -123,7 +122,6 @@
|
|||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div
|
<div
|
||||||
bind:this={containerElem}
|
|
||||||
class="h-12 rounded border border-dashed flex cursor-pointer"
|
class="h-12 rounded border border-dashed flex cursor-pointer"
|
||||||
class:h-26={uploadFiles.length > 0}
|
class:h-26={uploadFiles.length > 0}
|
||||||
dropzone="copy"
|
dropzone="copy"
|
||||||
|
@ -162,7 +162,13 @@ import {
|
|||||||
type DeleteBlockedUserReq,
|
type DeleteBlockedUserReq,
|
||||||
deleteBlockedUser
|
deleteBlockedUser
|
||||||
} from '@db/schema/blockedUser.ts';
|
} from '@db/schema/blockedUser.ts';
|
||||||
import { addReportAttachment, type AddReportAttachmentReq, reportAttachment } from '@db/schema/reportAttachment.ts';
|
import {
|
||||||
|
addReportAttachment,
|
||||||
|
type AddReportAttachmentReq,
|
||||||
|
getReportAttachments,
|
||||||
|
type GetReportAttachmentsReq,
|
||||||
|
reportAttachment
|
||||||
|
} from '@db/schema/reportAttachment.ts';
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
protected readonly db: MySql2Database<{
|
protected readonly db: MySql2Database<{
|
||||||
@ -280,6 +286,7 @@ export class Database {
|
|||||||
|
|
||||||
/* report attachment */
|
/* report attachment */
|
||||||
addReportAttachment = (values: AddReportAttachmentReq) => addReportAttachment(this.db, values);
|
addReportAttachment = (values: AddReportAttachmentReq) => addReportAttachment(this.db, values);
|
||||||
|
getReportAttachments = (values: GetReportAttachmentsReq) => getReportAttachments(this.db, values);
|
||||||
|
|
||||||
/* report status */
|
/* report status */
|
||||||
getReportStatus = (values: GetReportStatusReq) => getReportStatus(this.db, values);
|
getReportStatus = (values: GetReportStatusReq) => getReportStatus(this.db, values);
|
||||||
|
@ -21,7 +21,6 @@ export type AddReportAttachmentReq = {
|
|||||||
|
|
||||||
export type GetReportAttachmentsReq = {
|
export type GetReportAttachmentsReq = {
|
||||||
reportId: number;
|
reportId: number;
|
||||||
fileIds: number[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function addReportAttachment(db: Database, values: AddReportAttachmentReq) {
|
export async function addReportAttachment(db: Database, values: AddReportAttachmentReq) {
|
||||||
|
27
src/pages/admin/reports/attachment/[fileHash].ts
Normal file
27
src/pages/admin/reports/attachment/[fileHash].ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { Session } from '@util/session.ts';
|
||||||
|
import { Permissions } from '@util/permissions.ts';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { UPLOAD_PATH } from 'astro:env/server';
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params, cookies }) => {
|
||||||
|
Session.actionSessionFromCookies(cookies, Permissions.Reports);
|
||||||
|
|
||||||
|
if (!UPLOAD_PATH) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
const fileHash = params.fileHash as string;
|
||||||
|
const filePath = path.join(UPLOAD_PATH, fileHash);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
const fileStat = fs.statSync(filePath);
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
return new Response(fileStream as any, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Length': fileStat.size.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
Reference in New Issue
Block a user