diff --git a/.env.example b/.env.example index 8a842af..2ecc2a7 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ ADMIN_USER=admin ADMIN_PASSWORD=admin ADMIN_COOKIE=muelleel +UPLOAD_PATH=/tmp + YOUTUBE_INTRO_LINK=https://www.youtube-nocookie.com/embed/e78_QbTNb4s TEAMSPEAK_LINK=http://example.com DISCORD_LINK=http://example.com diff --git a/astro.config.mjs b/astro.config.mjs index a7a7d21..8029926 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -38,6 +38,9 @@ export default defineConfig({ ADMIN_PASSWORD: envField.string({ context: 'server', access: 'secret', optional: true }), ADMIN_COOKIE: envField.string({ context: 'server', access: 'secret', default: 'muelleel' }), + UPLOAD_PATH: envField.string({ context: 'server', access: 'secret', optional: true }), + MAX_UPLOAD_BYTES: envField.number({ context: 'server', access: 'secret', default: 20 * 1024 * 1024 }), + START_DATE: envField.string({ context: 'server', access: 'secret', default: '1970-01-01' }), WEBHOOK_ENDPOINT: envField.string({ context: 'server', access: 'secret', optional: true }), diff --git a/src/actions/feedback.ts b/src/actions/feedback.ts index f8df930..7678052 100644 --- a/src/actions/feedback.ts +++ b/src/actions/feedback.ts @@ -28,6 +28,15 @@ export const feedback = { }); } }), + submitFeedback: defineAction({ + input: z.object({ + urlHash: z.string(), + content: z.string() + }), + handler: async (input) => { + await db.submitFeedback(input); + } + }), feedbacks: defineAction({ handler: async (_, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Feedback); diff --git a/src/actions/report.ts b/src/actions/report.ts index 9f47f87..c0a7f29 100644 --- a/src/actions/report.ts +++ b/src/actions/report.ts @@ -1,10 +1,84 @@ -import { defineAction } from 'astro:actions'; +import { ActionError, defineAction } from 'astro:actions'; import { Session } from '@util/session.ts'; import { Permissions } from '@util/permissions.ts'; import { db } from '@db/database.ts'; import { z } from 'astro:schema'; +import { UPLOAD_PATH } from 'astro:env/server'; +import fs from 'node:fs'; +import crypto from 'node:crypto'; +import path from 'node:path'; +import { sendWebhook, WebhookAction } from '@util/webhook.ts'; export const report = { + submitReport: defineAction({ + input: z.object({ + urlHash: z.string(), + reason: z.string(), + body: z.string(), + files: z.array(z.instanceof(File)).nullable() + }), + handler: async (input) => { + const report = await db.getReportByUrlHash({ urlHash: input.urlHash }); + if (!report) { + throw new ActionError({ + code: 'NOT_FOUND' + }); + } + + if (!UPLOAD_PATH) { + throw new ActionError({ + code: 'FORBIDDEN', + message: 'Es dürfen keine Anhänge hochgeladen werden' + }); + } + + const filePaths = [] as string[]; + try { + await db.transaction(async (tx) => { + for (const file of input.files ?? []) { + const uuid = crypto.randomUUID(); + const tmpFilePath = path.join(UPLOAD_PATH!, uuid); + const tmpFileStream = fs.createWriteStream(tmpFilePath); + + filePaths.push(tmpFilePath); + + const md5Hash = crypto.createHash('md5'); + + for await (const chunk of file.stream()) { + md5Hash.update(chunk); + tmpFileStream.write(chunk); + } + + const hash = md5Hash.digest('hex'); + const filePath = path.join(UPLOAD_PATH!, hash); + + await tx.addReportAttachment({ + type: 'video', + hash: hash, + reportId: report.id + }); + + fs.renameSync(tmpFilePath, filePath); + filePaths.pop(); + filePaths.push(filePath); + } + + await tx.submitReport({ + urlHash: input.urlHash, + reason: input.reason, + body: input.body + }); + }); + } catch (e) { + for (const filePath of filePaths) { + fs.rmSync(filePath); + } + + throw e; + } + }, + accept: 'form' + }), addReport: defineAction({ input: z.object({ reason: z.string(), @@ -19,7 +93,7 @@ export const report = { const { id } = await db.addReport({ reason: input.reason, body: input.body, - createdAt: input.createdAt, + createdAt: input.createdAt ? new Date(input.createdAt) : null, reporterTeamId: input.reporter, reportedTeamId: input.reported }); @@ -52,6 +126,9 @@ export const report = { handler: async (input, context) => { Session.actionSessionFromCookies(context.cookies, Permissions.Reports); + let preReportStrike; + if (input.status === 'closed') preReportStrike = await db.getStrikeByReportId({ reportId: input.reportId }); + await db.transaction(async (tx) => { await tx.editReportStatus(input); @@ -62,6 +139,20 @@ export const report = { }); } }); + + if (input.status === 'closed' && preReportStrike?.strikeReasonId != input.strikeReasonId) { + const report = await db.getReportById({ id: input.reportId }); + if (report.reported) { + const strikes = await db.getStrikesByTeamId({ teamId: report.reported.id }); + const teamMembers = await db.getTeamMembersByTeamId({ teamId: report.reported.id }); + + // send webhook in background + sendWebhook(WebhookAction.Strike, { + users: teamMembers.map((tm) => tm.user.uuid!), + totalWeight: strikes.map((strike) => strike.reason.weight).reduce((a, b) => a + b, 0) + }); + } + } } }), reports: defineAction({ @@ -118,5 +209,29 @@ export const report = { strikeReasons: await db.getStrikeReasons({}) }; } + }), + teamNamesByUsername: defineAction({ + input: z.object({ + username: z.string().nullish() + }), + handler: async (input) => { + const teams = await db.getTeamsByUsername({ username: input.username ?? '', limit: 5 }); + + return { + teamNames: teams.map((team) => ({ name: team.user.username, value: team.team.name })) + }; + } + }), + teamNamesByTeamName: defineAction({ + input: z.object({ + teamName: z.string().nullish() + }), + handler: async (input) => { + const teams = await db.getTeams({ name: input.teamName, limit: 5 }); + + return { + teamNames: teams.map((team) => ({ name: team.name, value: team.name })) + }; + } }) }; diff --git a/src/app/website/report/AdversarySearch.svelte b/src/app/website/report/AdversarySearch.svelte new file mode 100644 index 0000000..81023ab --- /dev/null +++ b/src/app/website/report/AdversarySearch.svelte @@ -0,0 +1,77 @@ + + +