Compare commits
11 Commits
0280e2a277
...
main
Author | SHA1 | Date | |
---|---|---|---|
a6d910f56a | |||
fde50d21a6 | |||
8ea1750f1a | |||
5935b0d561 | |||
6e7c2eafca | |||
3c8dc30e43 | |||
8d8b1c52c0 | |||
1596fb605e | |||
7357ad9e88 | |||
3dd56bc471 | |||
8e60f83b6f |
@ -24,16 +24,16 @@ export class User extends Model {
|
||||
@Column({ type: DataTypes.DATE, allowNull: false })
|
||||
declare birthday: Date;
|
||||
@Column({ type: DataTypes.STRING })
|
||||
declare telephone: string;
|
||||
declare telephone: string | null;
|
||||
@Column({ type: DataTypes.STRING, allowNull: false })
|
||||
declare username: string;
|
||||
@Column({ type: DataTypes.ENUM('java', 'bedrock', 'noauth'), allowNull: false })
|
||||
declare playertype: 'java' | 'bedrock' | 'noauth';
|
||||
@Column({ type: DataTypes.STRING })
|
||||
declare password: string;
|
||||
declare password: string | null;
|
||||
@Column({ type: DataTypes.UUID, unique: true })
|
||||
@Index
|
||||
declare uuid: string;
|
||||
declare uuid: string | null;
|
||||
}
|
||||
|
||||
@Table({ modelName: 'report', underscored: true })
|
||||
@ -43,16 +43,16 @@ export class Report extends Model {
|
||||
declare url_hash: string;
|
||||
@Column({ type: DataTypes.STRING, allowNull: false })
|
||||
declare subject: string;
|
||||
@Column({ type: DataTypes.STRING })
|
||||
declare body: string;
|
||||
@Column({ type: DataTypes.TEXT })
|
||||
declare body: string | null;
|
||||
@Column({ type: DataTypes.BOOLEAN, allowNull: false })
|
||||
declare draft: boolean;
|
||||
@Column({ type: DataTypes.ENUM('none', 'review', 'reviewed'), allowNull: false })
|
||||
declare status: 'none' | 'review' | 'reviewed';
|
||||
@Column({ type: DataTypes.STRING })
|
||||
declare notice: string;
|
||||
@Column({ type: DataTypes.STRING })
|
||||
declare statement: string;
|
||||
declare notice: string | null;
|
||||
@Column({ type: DataTypes.TEXT })
|
||||
declare statement: string | null;
|
||||
@Column({ type: DataTypes.DATE })
|
||||
declare striked_at: Date | null;
|
||||
@Column({ type: DataTypes.INTEGER, allowNull: false })
|
||||
@ -60,10 +60,10 @@ export class Report extends Model {
|
||||
declare reporter_id: number;
|
||||
@Column({ type: DataTypes.INTEGER })
|
||||
@ForeignKey(() => User)
|
||||
declare reported_id: number;
|
||||
declare reported_id: number | null;
|
||||
@Column({ type: DataTypes.INTEGER })
|
||||
@ForeignKey(() => Admin)
|
||||
declare auditor_id: number;
|
||||
declare auditor_id: number | null;
|
||||
@Column({ type: DataTypes.INTEGER })
|
||||
@ForeignKey(() => StrikeReason)
|
||||
declare strike_reason_id: number | null;
|
||||
@ -72,22 +72,22 @@ export class Report extends Model {
|
||||
onDelete: 'CASCADE',
|
||||
foreignKey: 'reporter_id'
|
||||
})
|
||||
declare reporter: User;
|
||||
declare reporter: User | null;
|
||||
@BelongsTo(() => User, {
|
||||
onDelete: 'CASCADE',
|
||||
foreignKey: 'reported_id'
|
||||
})
|
||||
declare reported: User;
|
||||
declare reported: User | null;
|
||||
@BelongsTo(() => Admin, {
|
||||
onDelete: 'CASCADE',
|
||||
foreignKey: 'auditor_id'
|
||||
})
|
||||
declare auditor: Admin;
|
||||
declare auditor: Admin | null;
|
||||
@BelongsTo(() => StrikeReason, {
|
||||
onDelete: 'CASCADE',
|
||||
foreignKey: 'strike_reason_id'
|
||||
})
|
||||
declare strike_reason: StrikeReason;
|
||||
declare strike_reason: StrikeReason | null;
|
||||
}
|
||||
|
||||
@Table({ modelName: 'strike_reason', underscored: true, createdAt: false, updatedAt: false })
|
||||
@ -113,19 +113,21 @@ export class Feedback extends Model {
|
||||
@Column({ type: DataTypes.STRING, allowNull: false })
|
||||
declare event: string;
|
||||
@Column({ type: DataTypes.STRING })
|
||||
declare content: string;
|
||||
declare title: string | null;
|
||||
@Column({ type: DataTypes.TEXT })
|
||||
declare content: string | null;
|
||||
@Column({ type: DataTypes.STRING, allowNull: false, unique: true })
|
||||
@Index
|
||||
declare url_hash: string;
|
||||
@Column({ type: DataTypes.INTEGER })
|
||||
@ForeignKey(() => User)
|
||||
declare user_id: number;
|
||||
declare user_id: number | null;
|
||||
|
||||
@BelongsTo(() => User, {
|
||||
onDelete: 'CASCADE',
|
||||
foreignKey: 'user_id'
|
||||
})
|
||||
declare user: User;
|
||||
declare user: User | null;
|
||||
}
|
||||
|
||||
@Table({ modelName: 'admin', underscored: true })
|
||||
|
@ -82,7 +82,7 @@
|
||||
|
||||
let pageTitleSuffix = $derived(
|
||||
tabs.find((t) => $page.url.pathname === t.path)?.name ??
|
||||
($page.url.pathname === `${env.PUBLIC_BASE_PATH}/admin/login` ? 'Login' : null)
|
||||
($page.url.pathname === `${env.PUBLIC_BASE_PATH}/admin/login` ? 'Login' : null)
|
||||
);
|
||||
</script>
|
||||
|
||||
|
@ -68,12 +68,13 @@
|
||||
<hr class="divider my-1 mx-8 border-none" />
|
||||
<table class="table table-fixed h-fit">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Nutzer</th>
|
||||
<th>Datum</th>
|
||||
<th>Inhalt</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Titel</th>
|
||||
<th>Nutzer</th>
|
||||
<th>Datum</th>
|
||||
<th>Inhalt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<PaginationTableBody
|
||||
onUpdate={async () =>
|
||||
@ -91,6 +92,7 @@
|
||||
}}
|
||||
>
|
||||
<td title={feedback.event}>{feedback.event}</td>
|
||||
<td class="overflow-hidden overflow-ellipsis">{feedback.title}</td>
|
||||
<td class="flex">
|
||||
{feedback.user?.username || ''}
|
||||
{#if feedback.user}
|
||||
@ -108,15 +110,17 @@
|
||||
{/if}
|
||||
</td>
|
||||
<td
|
||||
>{new Intl.DateTimeFormat('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(new Date(feedback.updatedAt))} Uhr</td
|
||||
>{new Intl.DateTimeFormat('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(new Date(feedback.updatedAt))} Uhr</td
|
||||
>
|
||||
<td class="overflow-hidden overflow-ellipsis"
|
||||
>{feedback.content}{feedback.content_stripped ? '...' : ''}</td
|
||||
>
|
||||
<td>{feedback.content}...</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</PaginationTableBody>
|
||||
@ -167,6 +171,18 @@
|
||||
</div>
|
||||
<h3 class="font-roboto font-semibold text-2xl mb-2">Feedback</h3>
|
||||
<div class="w-full">
|
||||
<Input readonly={true} size="sm" value={activeFeedback.event} pickyWidth={false}>
|
||||
{#snippet label()}
|
||||
<span>Event</span>
|
||||
{/snippet}
|
||||
</Input>
|
||||
<Input readonly={true} size="sm" value={activeFeedback.title} pickyWidth={false}>
|
||||
{#snippet label()}
|
||||
<span>Titel</span>
|
||||
{/snippet}
|
||||
</Input>
|
||||
<Textarea readonly={true} rows={4} label="Inhalt" value={activeFeedback.content} />
|
||||
<div class="divider mb-1"></div>
|
||||
<Input
|
||||
readonly={true}
|
||||
size="sm"
|
||||
@ -177,7 +193,6 @@
|
||||
<span>Nutzer</span>
|
||||
{/snippet}
|
||||
</Input>
|
||||
<Textarea readonly={true} rows={4} label="Inhalt" value={activeFeedback.content} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -42,10 +42,14 @@ export const POST = (async ({ request, cookies }) => {
|
||||
attributes: data.preview
|
||||
? {
|
||||
exclude: ['content'],
|
||||
include: [[sequelize.literal('SUBSTR(content, 1, 50)'), 'content']]
|
||||
include: [
|
||||
[sequelize.literal('SUBSTR(content, 1, 50)'), 'content'],
|
||||
[sequelize.literal('LENGTH(content) > 50'), 'content_stripped']
|
||||
]
|
||||
}
|
||||
: undefined,
|
||||
include: { model: User, as: 'user' },
|
||||
order: [['created_at', 'DESC']],
|
||||
offset: data.hash ? 0 : data.from || 0,
|
||||
limit: data.hash ? 1 : data.limit || 100
|
||||
});
|
||||
|
@ -125,14 +125,14 @@
|
||||
<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>
|
||||
<tr>
|
||||
<th>Grund</th>
|
||||
<th>Ersteller</th>
|
||||
<th>Reporteter User</th>
|
||||
<th>Datum</th>
|
||||
<th>Bearbeitungsstatus</th>
|
||||
<th>Reportstatus</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<PaginationTableBody
|
||||
onUpdate={async () =>
|
||||
@ -182,13 +182,13 @@
|
||||
{/if}
|
||||
</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
|
||||
>{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'
|
||||
@ -305,7 +305,7 @@
|
||||
<option
|
||||
value="none"
|
||||
disabled={activeReport.auditor != null || activeReport.notice || activeReport.statement}
|
||||
>Unbearbeitet</option
|
||||
>Unbearbeitet</option
|
||||
>
|
||||
<option value="review">In Bearbeitung</option>
|
||||
<option value="reviewed">Bearbeitet</option>
|
||||
|
@ -50,12 +50,7 @@ export const POST = (async ({ request, cookies }) => {
|
||||
|
||||
let reports = await Report.findAll({
|
||||
where: reportFindOptions,
|
||||
include: [
|
||||
{ model: User, as: 'reporter' },
|
||||
{ model: User, as: 'reported' },
|
||||
{ model: Admin, as: 'auditor' },
|
||||
{ model: StrikeReason, as: 'strike_reason' }
|
||||
],
|
||||
include: [{ all: true }],
|
||||
order: [['created_at', 'DESC']],
|
||||
offset: data.from || 0,
|
||||
limit: data.limit || 100
|
||||
@ -102,64 +97,61 @@ export const PATCH = (async ({ request, cookies }) => {
|
||||
}
|
||||
const data = parseResult.data;
|
||||
|
||||
const report = await Report.findOne({ where: { id: data.id } });
|
||||
const report = await Report.findOne({ where: { id: data.id }, include: [{ all: true }] });
|
||||
const admin = await Admin.findOne({ where: { id: data.auditor } });
|
||||
const reported = data.reported
|
||||
? await User.findOne({ where: { uuid: data.reported } })
|
||||
: undefined;
|
||||
if (report === null || (admin === null && data.auditor != -1) || reported === null)
|
||||
const reported = data.reported ? await User.findOne({ where: { uuid: data.reported } }) : null;
|
||||
if (report === null || (admin === null && data.auditor != -1))
|
||||
return new Response(null, { status: 400 });
|
||||
|
||||
const webhookTriggerUsers: string[] = [];
|
||||
if (report.reported_id != reported?.id) {
|
||||
const oldReportUser = await User.findByPk(report.reported_id);
|
||||
if (oldReportUser) webhookTriggerUsers.push(oldReportUser.uuid);
|
||||
if (reported) webhookTriggerUsers.push(reported.uuid);
|
||||
} else if (
|
||||
reported &&
|
||||
report.reported_id != null &&
|
||||
report.strike_reason_id != data.strike_reason
|
||||
|
||||
// check if strike reason has changed and return 400 if it doesn't exist
|
||||
if (
|
||||
(report.strike_reason?.id ?? -1) != data.strike_reason &&
|
||||
data.strike_reason != null &&
|
||||
data.strike_reason != -1
|
||||
) {
|
||||
webhookTriggerUsers.push(reported.uuid);
|
||||
const strike_reason = await StrikeReason.findByPk(data.strike_reason);
|
||||
if (strike_reason == null) return new Response(null, { status: 400 });
|
||||
}
|
||||
|
||||
if (data.status === 'reviewed') {
|
||||
// trigger webhook if status changed to reviewed
|
||||
if (report.status !== 'reviewed' && data.strike_reason != -1 && reported) {
|
||||
webhookTriggerUsers.push(reported.uuid!);
|
||||
}
|
||||
// trigger webhook if strike reason has changed
|
||||
else if (
|
||||
(report.strike_reason?.id ?? -1) != data.strike_reason &&
|
||||
report.reported &&
|
||||
reported
|
||||
) {
|
||||
webhookTriggerUsers.push(reported.uuid!);
|
||||
}
|
||||
} else if (report.status === 'reviewed') {
|
||||
// trigger webhook if report status is reviewed and reported user has changed
|
||||
if (report.strike_reason != null && report.reported) {
|
||||
webhookTriggerUsers.push(report.reported.uuid!);
|
||||
}
|
||||
}
|
||||
|
||||
report.reported_id = reported?.id ?? null;
|
||||
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 (data.strike_reason != null) {
|
||||
if (data.status !== 'reviewed') {
|
||||
if (data.strike_reason == -1) {
|
||||
report.strike_reason_id = null;
|
||||
} else {
|
||||
const strike_reason = await StrikeReason.findByPk(data.strike_reason);
|
||||
if (strike_reason == null) return new Response(null, { status: 400 });
|
||||
report.strike_reason_id = strike_reason.id;
|
||||
}
|
||||
} else if (data.strike_reason == -1 && report.strike_reason_id != null) {
|
||||
report.strike_reason_id = null;
|
||||
report.striked_at = null;
|
||||
} else if (data.strike_reason != -1 && data.strike_reason != report.strike_reason_id) {
|
||||
if (!report.reported_id) return new Response(null, { status: 400 });
|
||||
const strike_reason = await StrikeReason.findByPk(data.strike_reason);
|
||||
if (strike_reason == null) return new Response(null, { status: 400 });
|
||||
report.strike_reason_id = strike_reason.id;
|
||||
report.striked_at = new Date(Date.now());
|
||||
}
|
||||
}
|
||||
if (data.reported != null && reported) report.reported_id = reported.id;
|
||||
if (data.strike_reason != null)
|
||||
report.strike_reason_id = data.strike_reason == -1 ? null : data.strike_reason;
|
||||
if (data.strike_reason != null)
|
||||
report.striked_at = data.strike_reason == -1 ? null : new Date(Date.now());
|
||||
if (admin != null) report.auditor_id = admin.id;
|
||||
|
||||
await report.save();
|
||||
|
||||
if (webhookTriggerUsers.length > 0 && data.status == 'reviewed' && env.REPORTED_WEBHOOK) {
|
||||
for (const webhookTriggerUser of webhookTriggerUsers) {
|
||||
try {
|
||||
// no `await` to avoid blocking
|
||||
webhookUserReported(env.REPORTED_WEBHOOK, webhookTriggerUser);
|
||||
} catch (e) {
|
||||
console.error(`failed to send reported webhook: ${e}`);
|
||||
}
|
||||
}
|
||||
for (const webhookTriggerUser of webhookTriggerUsers) {
|
||||
// no `await` to avoid blocking
|
||||
webhookUserReported(env.REPORTED_WEBHOOK, webhookTriggerUser).catch((e) =>
|
||||
console.error(`failed to send reported webhook: ${e}`)
|
||||
);
|
||||
}
|
||||
|
||||
return new Response();
|
||||
|
@ -95,19 +95,19 @@
|
||||
<div class="h-full overflow-scroll" bind:this={userTableContainerElement}>
|
||||
<table class="table table-auto">
|
||||
<thead>
|
||||
<!-- prettier-ignore -->
|
||||
<SortableTr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0">
|
||||
<th></th>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'firstname', asc: e.asc}}}>Vorname</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'lastname', asc: e.asc}}}>Nachname</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'birthday', asc: e.asc}}}>Geburtstag</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'telephone', asc: e.asc}}}>Telefon</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'username', asc: e.asc}}}>Username</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'playertype', asc: e.asc}}}>Minecraft Edition</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'password', asc: e.asc}}}>Passwort</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'uuid', asc: e.asc}}}>UUID</SortableTh>
|
||||
<th></th>
|
||||
</SortableTr>
|
||||
<!-- prettier-ignore -->
|
||||
<SortableTr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0">
|
||||
<th></th>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'firstname', asc: e.asc}}}>Vorname</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'lastname', asc: e.asc}}}>Nachname</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'birthday', asc: e.asc}}}>Geburtstag</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'telephone', asc: e.asc}}}>Telefon</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'username', asc: e.asc}}}>Username</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'playertype', asc: e.asc}}}>Minecraft Edition</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'password', asc: e.asc}}}>Passwort</SortableTh>
|
||||
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'uuid', asc: e.asc}}}>UUID</SortableTh>
|
||||
<th></th>
|
||||
</SortableTr>
|
||||
</thead>
|
||||
<PaginationTableBody
|
||||
onUpdate={async () => {
|
||||
|
@ -21,9 +21,10 @@ export const POST = (async ({ request, url }) => {
|
||||
where: { uuid: data.users },
|
||||
attributes: ['id', 'uuid']
|
||||
})) {
|
||||
feedback[user.uuid] = {
|
||||
feedback[user.uuid!] = {
|
||||
url_hash: crypto.randomBytes(18).toString('hex'),
|
||||
event: data.event,
|
||||
title: data.title ?? null,
|
||||
draft: true,
|
||||
user_id: user.id
|
||||
};
|
||||
|
@ -2,5 +2,6 @@ import { z } from 'zod';
|
||||
|
||||
export const FeedbackAddSchema = z.object({
|
||||
event: z.string(),
|
||||
title: z.string().nullish(),
|
||||
users: z.array(z.string())
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
|
||||
return {
|
||||
draft: feedback.content === null,
|
||||
event: feedback.event,
|
||||
title: feedback.title,
|
||||
anonymous: feedback.user_id === null
|
||||
};
|
||||
};
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
{#if draft}
|
||||
<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
|
||||
<FeedbackDraft event={data.event} anonymous={data.anonymous} onSubmit={() => (draft = false)} />
|
||||
<FeedbackDraft title={data.title} anonymous={data.anonymous} onSubmit={() => (draft = false)} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}>
|
||||
|
@ -5,8 +5,11 @@
|
||||
import { page } from '$app/stores';
|
||||
import { getPopupModalShowFn } from '$lib/context';
|
||||
|
||||
let { event, anonymous, onSubmit }: { event: string; anonymous: boolean; onSubmit?: () => void } =
|
||||
$props();
|
||||
let {
|
||||
title,
|
||||
anonymous,
|
||||
onSubmit
|
||||
}: { title: string | null; anonymous: boolean; onSubmit?: () => void } = $props();
|
||||
|
||||
let showPopupModal = getPopupModalShowFn();
|
||||
|
||||
@ -47,11 +50,13 @@
|
||||
}}
|
||||
>
|
||||
<div class="space-y-4 my-4">
|
||||
<Input size="sm" pickyWidth={false} disabled value={event}>
|
||||
{#snippet label()}
|
||||
<span>Event</span>
|
||||
{/snippet}
|
||||
</Input>
|
||||
{#if title}
|
||||
<Input size="sm" pickyWidth={false} disabled value={title}>
|
||||
{#snippet label()}
|
||||
<span>Event</span>
|
||||
{/snippet}
|
||||
</Input>
|
||||
{/if}
|
||||
<Textarea required={true} rows={4} label="Feedback" bind:value={content} />
|
||||
{#if !anonymous}
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
|
Reference in New Issue
Block a user