Compare commits

..

9 Commits

Author SHA1 Message Date
a6d910f56a sort feedback desc
All checks were successful
delpoy / build-and-deploy (push) Successful in 51s
2024-12-29 00:19:24 +01:00
fde50d21a6 fuck that shit, revert
All checks were successful
delpoy / build-and-deploy (push) Successful in 1m3s
2024-12-28 03:46:17 +01:00
8ea1750f1a fix scroll position resetting on admin panel
All checks were successful
delpoy / build-and-deploy (push) Successful in 1m2s
2024-12-28 02:26:43 +01:00
5935b0d561 stick header and filter bar to top
All checks were successful
delpoy / build-and-deploy (push) Successful in 48s
2024-12-28 01:29:05 +01:00
6e7c2eafca fix reported user not updating when changed
All checks were successful
delpoy / build-and-deploy (push) Successful in 45s
2024-12-28 00:51:36 +01:00
3c8dc30e43 increase string size for some database fields
All checks were successful
delpoy / build-and-deploy (push) Successful in 48s
2024-12-28 00:44:12 +01:00
8d8b1c52c0 make feedback title optional
All checks were successful
delpoy / build-and-deploy (push) Successful in 54s
2024-12-27 19:00:22 +01:00
1596fb605e update report admin endpoint
All checks were successful
delpoy / build-and-deploy (push) Successful in 56s
2024-12-24 01:18:36 +01:00
7357ad9e88 actually fix strike date not set if status changed but strike reason not
All checks were successful
delpoy / build-and-deploy (push) Successful in 39s
2024-12-20 21:07:36 +01:00
12 changed files with 142 additions and 118 deletions

View File

@ -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 })

View File

@ -70,6 +70,7 @@
<thead>
<tr>
<th>Event</th>
<th>Titel</th>
<th>Nutzer</th>
<th>Datum</th>
<th>Inhalt</th>
@ -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}
@ -116,7 +118,9 @@
minute: '2-digit'
}).format(new Date(feedback.updatedAt))} Uhr</td
>
<td>{feedback.content}...</td>
<td class="overflow-hidden overflow-ellipsis"
>{feedback.content}{feedback.content_stripped ? '...' : ''}</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}

View File

@ -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
});

View File

@ -97,66 +97,62 @@ 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 || data.status != report.status)
) {
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) {
// no `await` to avoid blocking
webhookUserReported(env.REPORTED_WEBHOOK, webhookTriggerUser).catch((e) =>
console.error(`failed to send reported webhook: ${e}`)
);
}
}
return new Response();
}) satisfies RequestHandler;

View File

@ -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
};

View File

@ -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())
});

View File

@ -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
};
};

View File

@ -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 }}>

View File

@ -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}>
{#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">