diff --git a/README.md b/README.md
index 20e3226..47f20a6 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,79 @@
+
+POST
/api/report
(Erstellt einen Report)
+
+##### Request Body
+
+```
+{
+ // UUID des Report Erstellers
+ "reporter": string,
+ // UUID des Reporteten Spielers
+ "reported": string | null,
+ // Report Grund
+ "reason": string
+}
+```
+
+##### Response Codes
+
+| http code | beschreibung |
+| --------- | ----------------------------------------------------------------- |
+| 200 | / |
+| 400 | Der Request Body ist falsch |
+| 401 | Es wurde ein falsches API Secret angegeben |
+| 404 | Der Report Ersteller, oder der reportete Spieler, existiert nicht |
+
+##### Response Body
+
+```
+{
+ // URL, wo der Ersteller den Report abschicken kann
+ "url": string
+}
+```
+
+
+
+
+PUT
/api/report
(Erstellt einen Abgeschlossenen Report)
+
+##### Request Body
+
+```
+{
+ // UUID des Reporters. Wenn `null`, wird der Reporter als System interpretiert
+ "reporter": string | null,
+ // UUID des Reporteten Spielers
+ "reported": string,
+ // Report Grund
+ "reason": string,
+ // Inhalt des Reports
+ "body": string | null,
+ // Interne Notiz
+ "notice": string | null,
+ // Öffentliches Statement
+ "statement": string | null,
+ // ID des Strikegrundes
+ "strike_reason_id": number
+}
+```
+
+| http code | beschreibung |
+| --------- | ----------------------------------------------------------------- |
+| 200 | / |
+| 400 | Der Request Body ist falsch |
+| 401 | Es wurde ein falsches API Secret angegeben |
+| 404 | Der Report Ersteller, oder der reportete Spieler, existiert nicht |
+
+##### Response Body
+
+`/`
+
+
+
POST
/api/player/death
(Registriert einen Spielertod)
diff --git a/src/actions/report.ts b/src/actions/report.ts
index d3d3a0a..9f47f87 100644
--- a/src/actions/report.ts
+++ b/src/actions/report.ts
@@ -47,12 +47,21 @@ export const report = {
status: z.enum(['open', 'closed']).nullable(),
notice: z.string().nullable(),
statement: z.string().nullable(),
- strikeId: z.number().nullable()
+ strikeReasonId: z.number().nullable()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);
- await db.editReportStatus(input);
+ await db.transaction(async (tx) => {
+ await tx.editReportStatus(input);
+
+ if (input.strikeReasonId) {
+ await db.editStrike({
+ reportId: input.reportId,
+ strikeReasonId: input.strikeReasonId
+ });
+ }
+ });
}
}),
reports: defineAction({
diff --git a/src/app/admin/reports/BottomBar.svelte b/src/app/admin/reports/BottomBar.svelte
index 5d2cfee..95ccd5e 100644
--- a/src/app/admin/reports/BottomBar.svelte
+++ b/src/app/admin/reports/BottomBar.svelte
@@ -20,6 +20,7 @@
let status = $state<'open' | 'closed' | null>(null);
let notice = $state(null);
let statement = $state(null);
+ let strikeReason = $state(null);
// consts
const strikeReasonValues = strikeReasons.reduce(
@@ -50,7 +51,7 @@
status: status,
notice: notice,
statement: statement,
- strikeId: null
+ strikeReasonId: Number(strikeReason)
} as ReportStatus)
};
}
@@ -75,12 +76,14 @@
diff --git a/src/app/admin/reports/reports.ts b/src/app/admin/reports/reports.ts
index f8d3974..75cd1b7 100644
--- a/src/app/admin/reports/reports.ts
+++ b/src/app/admin/reports/reports.ts
@@ -9,7 +9,7 @@ export type Report = Reports[0];
export type ReportStatus = Exclude<
Exclude['data'], undefined>['reportStatus'],
null
->;
+> & { strikeReasonId: number | null };
export type StrikeReasons = Exclude<
ActionReturnType['data'],
@@ -65,7 +65,7 @@ export async function editReportStatus(report: Report, reportStatus: ReportStatu
status: reportStatus.status,
notice: reportStatus.notice,
statement: reportStatus.statement,
- strikeId: reportStatus.strikeId
+ strikeReasonId: reportStatus.strikeReasonId
});
if (error) {
diff --git a/src/db/database.sql b/src/db/database.sql
index b9e4431..fab0f60 100644
--- a/src/db/database.sql
+++ b/src/db/database.sql
@@ -68,21 +68,6 @@ CREATE TABLE IF NOT EXISTS death (
FOREIGN KEY (killer_user_id) REFERENCES user(id) ON DELETE CASCADE
);
--- strike reason
-CREATE TABLE IF NOT EXISTS strike_reason (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- weight TINYINT NOT NULL
-);
-
--- strike
-CREATE TABLE IF NOT EXISTS strike (
- id INT AUTO_INCREMENT PRIMARY KEY,
- at TIMESTAMP NOT NULL,
- strike_reason_id INT NOT NULL,
- FOREIGN KEY (strike_reason_id) REFERENCES strike_reason(id) ON DELETE CASCADE
-);
-
-- report
CREATE TABLE IF NOT EXISTS report (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -90,7 +75,7 @@ CREATE TABLE IF NOT EXISTS report (
body TEXT,
url_hash VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP,
- reporter_team_id INT NOT NULL,
+ reporter_team_id INT,
reported_team_id INT,
FOREIGN KEY (reporter_team_id) REFERENCES team(id) ON DELETE CASCADE,
FOREIGN KEY (reported_team_id) REFERENCES team(id) ON DELETE CASCADE
@@ -103,10 +88,24 @@ CREATE TABLE IF NOT EXISTS report_status (
statement TEXT,
report_id INT NOT NULL UNIQUE,
reviewer_id INT,
- strike_id INT,
FOREIGN KEY (report_id) REFERENCES report(id) ON DELETE CASCADE,
- FOREIGN KEY (reviewer_id) REFERENCES admin(id) ON DELETE CASCADE,
- FOREIGN KEY (strike_id) REFERENCES strike(id) ON DELETE CASCADE
+ FOREIGN KEY (reviewer_id) REFERENCES admin(id) ON DELETE CASCADE
+);
+
+-- strike reason
+CREATE TABLE IF NOT EXISTS strike_reason (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ weight TINYINT NOT NULL
+);
+
+-- strike
+CREATE TABLE IF NOT EXISTS strike (
+ at TIMESTAMP NOT NULL,
+ report_id INT NOT NULL UNIQUE,
+ strike_reason_id INT NOT NULL,
+ FOREIGN KEY (report_id) REFERENCES report(id) ON DELETE CASCADE,
+ FOREIGN KEY (strike_reason_id) REFERENCES strike_reason(id) ON DELETE CASCADE
);
-- feedback
diff --git a/src/db/database.ts b/src/db/database.ts
index 7c5915d..dd662b8 100644
--- a/src/db/database.ts
+++ b/src/db/database.ts
@@ -109,7 +109,13 @@ import {
type DeleteStrikeReasonReq,
deleteStrikeReason
} from '@db/schema/strikeReason.ts';
-import { getStrikesByTeamId, type GetStrikesByTeamId, strike } from '@db/schema/strike.ts';
+import {
+ editStrike,
+ type EditStrikeReq,
+ getStrikesByTeamId,
+ type GetStrikesByTeamIdReq,
+ strike
+} from '@db/schema/strike.ts';
import {
editReportStatus,
type EditReportStatusReq,
@@ -248,7 +254,8 @@ export class Database {
getStrikeReasons = (values: GetStrikeReasonsReq) => getStrikeReasons(this.db, values);
/* strikes */
- getStrikesByTeamId = (values: GetStrikesByTeamId) => getStrikesByTeamId(this.db, values);
+ editStrike = (values: EditStrikeReq) => editStrike(this.db, values);
+ getStrikesByTeamId = (values: GetStrikesByTeamIdReq) => getStrikesByTeamId(this.db, values);
/* feedback */
addFeedback = (values: AddFeedbackReq) => addFeedback(this.db, values);
diff --git a/src/db/schema/report.ts b/src/db/schema/report.ts
index 222178d..d79f729 100644
--- a/src/db/schema/report.ts
+++ b/src/db/schema/report.ts
@@ -1,10 +1,10 @@
import { alias, int, mysqlTable, text, timestamp, varchar } from 'drizzle-orm/mysql-core';
-import { strike } from './strike.ts';
import type { MySql2Database } from 'drizzle-orm/mysql2';
import { and, eq } from 'drizzle-orm';
import { reportStatus } from './reportStatus.ts';
import { generateRandomString } from '@util/random.ts';
import { team } from '@db/schema/team.ts';
+import { BASE_PATH } from 'astro:env/server';
type Database = MySql2Database<{ report: typeof report }>;
@@ -14,9 +14,7 @@ export const report = mysqlTable('report', {
body: text('body'),
urlHash: varchar('url_hash', { length: 255 }).notNull(),
createdAt: timestamp('created_at', { mode: 'string' }),
- reporterTeamId: int('reporter_team_id')
- .notNull()
- .references(() => team.id),
+ reporterTeamId: int('reporter_team_id').references(() => team.id),
reportedTeamId: int('reported_team_id').references(() => team.id)
});
@@ -24,7 +22,7 @@ export type AddReportReq = {
reason: string;
body: string | null;
createdAt?: string | null;
- reporterTeamId: number;
+ reporterTeamId?: number;
reportedTeamId?: number | null;
};
@@ -34,19 +32,21 @@ export type GetReportsReq = {
};
export async function addReport(db: Database, values: AddReportReq) {
+ const urlHash = generateRandomString(16);
+
const r = await db
.insert(report)
.values({
reason: values.reason,
body: values.body,
- urlHash: generateRandomString(16),
+ urlHash: urlHash,
createdAt: values.createdAt,
reporterTeamId: values.reporterTeamId,
reportedTeamId: values.reportedTeamId
})
.$returningId();
- return r[0];
+ return Object.assign(r[0], { url: `${BASE_PATH}/report/${urlHash}` });
}
export async function getReports(db: Database, values: GetReportsReq) {
@@ -94,7 +94,6 @@ export async function getReports(db: Database, values: GetReportsReq) {
.innerJoin(reporterTeam, eq(report.reporterTeamId, reporterTeam.id))
.leftJoin(reportedTeam, eq(report.reportedTeamId, reportedTeam.id))
.leftJoin(reportStatus, eq(report.id, reportStatus.reportId))
- .leftJoin(strike, eq(reportStatus.strikeId, strike.id))
.where(
and(
values.reporter != null ? eq(report.reporterTeamId, reporterIdSubquery!.id) : undefined,
diff --git a/src/db/schema/reportStatus.ts b/src/db/schema/reportStatus.ts
index 731583b..2824dc9 100644
--- a/src/db/schema/reportStatus.ts
+++ b/src/db/schema/reportStatus.ts
@@ -1,5 +1,4 @@
import { int, mysqlEnum, mysqlTable, text } from 'drizzle-orm/mysql-core';
-import { strike } from './strike.ts';
import { admin } from './admin.ts';
import { report } from './report.ts';
import type { MySql2Database } from 'drizzle-orm/mysql2';
@@ -15,8 +14,7 @@ export const reportStatus = mysqlTable('report_status', {
.notNull()
.unique()
.references(() => report.id),
- reviewerId: int('reviewer_id').references(() => admin.id),
- strikeId: int('strike_id').references(() => strike.id)
+ reviewerId: int('reviewer_id').references(() => admin.id)
});
export type GetReportStatusReq = {
@@ -28,7 +26,6 @@ export type EditReportStatusReq = {
status: 'open' | 'closed' | null;
notice: string | null;
statement: string | null;
- strikeId: number | null;
};
export async function getReportStatus(db: Database, values: GetReportStatusReq) {
@@ -47,8 +44,7 @@ export async function editReportStatus(db: Database, values: EditReportStatusReq
set: {
status: values.status,
notice: values.notice,
- statement: values.statement,
- strikeId: values.strikeId
+ statement: values.statement
}
});
}
diff --git a/src/db/schema/strike.ts b/src/db/schema/strike.ts
index 135cf73..7884bcb 100644
--- a/src/db/schema/strike.ts
+++ b/src/db/schema/strike.ts
@@ -8,27 +8,50 @@ import { reportStatus } from '@db/schema/reportStatus.ts';
type Database = MySql2Database<{ strike: typeof strike }>;
export const strike = mysqlTable('strike', {
- id: int('id').primaryKey().autoincrement(),
- at: timestamp('at', { mode: 'string' }).notNull(),
+ at: timestamp('at', { mode: 'date' }).notNull(),
+ reportId: int('report_id')
+ .notNull()
+ .references(() => report.id),
strikeReasonId: int('strike_reason_id')
.notNull()
.references(() => strikeReason.id)
});
-export type GetStrikesByTeamId = {
+export type EditStrikeReq = {
+ reportId: number;
+ at?: Date;
+ strikeReasonId: number;
+};
+
+export type GetStrikesByTeamIdReq = {
teamId: number;
};
-export async function getStrikesByTeamId(db: Database, values: GetStrikesByTeamId) {
+export async function editStrike(db: Database, values: EditStrikeReq) {
+ return db
+ .insert(strike)
+ .values({
+ at: values.at ?? new Date(),
+ reportId: values.reportId,
+ strikeReasonId: values.strikeReasonId
+ })
+ .onDuplicateKeyUpdate({
+ set: {
+ at: values.at ?? new Date(),
+ strikeReasonId: values.strikeReasonId
+ }
+ });
+}
+
+export async function getStrikesByTeamId(db: Database, values: GetStrikesByTeamIdReq) {
return db
.select({
- id: strike.id,
at: strike.at,
+ report: report,
reason: strikeReason
})
.from(strike)
.innerJoin(strikeReason, eq(strike.strikeReasonId, strikeReason.id))
- .innerJoin(reportStatus, eq(strike.id, reportStatus.strikeId))
.innerJoin(report, eq(reportStatus.reportId, report.id))
.where(eq(report.reportedTeamId, values.teamId));
}
diff --git a/src/db/schema/team.ts b/src/db/schema/team.ts
index 48b9a69..128328c 100644
--- a/src/db/schema/team.ts
+++ b/src/db/schema/team.ts
@@ -6,7 +6,6 @@ import { user } from './user.ts';
import { teamDraft } from './teamDraft.ts';
import { death } from '@db/schema/death.ts';
import { report } from '@db/schema/report.ts';
-import { reportStatus } from '@db/schema/reportStatus.ts';
import { strikeReason } from '@db/schema/strikeReason.ts';
import { strike } from '@db/schema/strike.ts';
@@ -142,8 +141,7 @@ export async function getTeamsFull(db: Database, _values: GetTeamsFullReq) {
})
.from(strike)
.innerJoin(strikeReason, eq(strike.strikeReasonId, strikeReason.id))
- .innerJoin(reportStatus, eq(strike.id, reportStatus.strikeId))
- .innerJoin(report, eq(reportStatus.reportId, report.id))
+ .innerJoin(report, eq(strike.reportId, report.id))
.innerJoin(team, eq(report.reportedTeamId, team.id))
.as('strike_weight_subquery');
diff --git a/src/pages/api/report/index.ts b/src/pages/api/report/index.ts
new file mode 100644
index 0000000..fe55371
--- /dev/null
+++ b/src/pages/api/report/index.ts
@@ -0,0 +1,96 @@
+import type { APIRoute } from 'astro';
+import { z } from 'astro:schema';
+import { API_SECRET } from 'astro:env/server';
+import { db } from '@db/database.ts';
+
+const postSchema = z.object({
+ reporter: z.string(),
+ reported: z.string().nullable(),
+ reason: z.string()
+});
+
+export const POST: APIRoute = async ({ request }) => {
+ if (API_SECRET && request.headers.get('authorization') !== `Basic ${API_SECRET}`) {
+ return new Response(null, { status: 401 });
+ }
+
+ let parsed;
+ try {
+ parsed = await postSchema.parseAsync(await request.json());
+ } catch (_) {
+ return new Response(null, { status: 400 });
+ }
+
+ const reporterTeam = await db.getTeamByUserUuid({ uuid: parsed.reporter });
+ if (!reporterTeam) return new Response(null, { status: 404 });
+
+ let reportedTeam = null;
+ if (parsed.reported) {
+ reportedTeam = await db.getTeamByUserUuid({ uuid: parsed.reported });
+ if (!reportedTeam) return new Response(null, { status: 404 });
+ }
+
+ const report = await db.addReport({
+ reporterTeamId: reporterTeam.team.id,
+ reportedTeamId: reportedTeam?.team.id,
+ reason: parsed.reason,
+ body: null
+ });
+
+ return new Response(JSON.stringify({ url: report.url }), { status: 200 });
+};
+
+const putSchema = z.object({
+ reporter: z.string().nullable(),
+ reported: z.string(),
+ reason: z.string(),
+ body: z.string().nullable(),
+ notice: z.string().nullable(),
+ statement: z.string().nullable(),
+ strike_reason_id: z.number()
+});
+
+export const PUT: APIRoute = async ({ request }) => {
+ if (API_SECRET && request.headers.get('authorization') !== `Basic ${API_SECRET}`) {
+ return new Response(null, { status: 401 });
+ }
+
+ let parsed;
+ try {
+ parsed = await putSchema.parseAsync(await request.json());
+ } catch (_) {
+ return new Response(null, { status: 400 });
+ }
+
+ let reporterTeam = null;
+ if (parsed.reported) {
+ reporterTeam = await db.getTeamByUserUuid({ uuid: parsed.reported });
+ if (!reporterTeam) return new Response(null, { status: 404 });
+ }
+
+ const reportedTeam = await db.getTeamByUserUuid({ uuid: parsed.reported });
+ if (!reportedTeam) return new Response(null, { status: 404 });
+
+ await db.transaction(async (tx) => {
+ const report = await tx.addReport({
+ reporterTeamId: reporterTeam?.team.id,
+ reportedTeamId: reportedTeam.team.id,
+ reason: parsed.reason,
+ body: parsed.body
+ });
+
+ await tx.editReportStatus({
+ reportId: report.id,
+ notice: parsed.notice,
+ statement: parsed.statement,
+ status: 'closed'
+ });
+
+ await tx.editStrike({
+ reportId: report.id,
+ strikeReasonId: parsed.strike_reason_id
+ });
+ });
+
+ return new Response(null, { status: 200 });
+};