rework api endpoints
All checks were successful
deploy / build-and-deploy (push) Successful in 19s

This commit is contained in:
2025-10-17 20:15:06 +02:00
parent a27cf5f35b
commit 964ccfacbf
10 changed files with 266 additions and 189 deletions

View File

@@ -42,7 +42,7 @@
</details> </details>
<details> <details>
<summary><code>POST</code> <code>/api/report</code> (Erstellt einen Report)</summary> <summary><code>POST</code> <code>/api/reports</code> (Erstellt einen Report)</summary>
##### Request Body ##### Request Body
@@ -78,7 +78,7 @@
</details> </details>
<details> <details>
<summary><code>PUT</code> <code>/api/report</code> (Erstellt einen Abgeschlossenen Report)</summary> <summary><code>PUT</code> <code>/api/reports</code> (Erstellt einen Abgeschlossenen Report)</summary>
##### Request Body ##### Request Body
@@ -117,16 +117,13 @@
</details> </details>
<details> <details>
<summary><code>POST</code> <code>/api/player</code> (Status eines Spielers)</summary> <summary><code>GET</code> <code>/api/users/{uuid}</code> (Status eines Spielers)</summary>
##### Request Body #### Path Parameters
``` | parameter | beschreibung |
{ | --------- | ------------------- |
// UUID eines Spielers | `uuid` | UUID eines Spielers |
"uuid": string
}
```
##### Response Codes ##### Response Codes
@@ -161,6 +158,59 @@
</details> </details>
<details>
<summary><code>GET</code> <code>/api/users/{uuid}/reports</code> (Reports eines Spielers)</summary>
#### Path Parameters
| parameter | beschreibung |
| --------- | ------------------- |
| `uuid` | UUID eines Spielers |
##### Response Codes
| http code | beschreibung |
| --------- | ------------------------------------------ |
| 200 | / |
| 400 | Der Request Body ist falsch |
| 401 | Es wurde ein falsches API Secret angegeben |
| 404 | Der Spieler existiert nicht |
##### Response Body
```
{
// Alle Reports, die der Spieler selber erstellt hat
"from_self": {
// Die UUID des reporteten Spielers oder null falls ein unbekannter Spieler reportet wurde
"reported": string | null,
// Grund des Reports
"reason": string,
// Wann der Report abgeschickt wurde als UTC Millisekunden oder null falls der Report noch nicht abgeschickt wurde (=> kann noch bearbeitet werden)
"created": number | null,
// Status des Reports, "open" wenn er gerade bearbeitet wird, "closed" falls er bearbeitet wurde, null wenn nichts von beidem
"status": "open" | "closed" | null,
// Url zum Report auf der Website
"url": string
}[],
// Alle Reports, die gegen den Spieler erstellt wurden
"to_self": {
// Die UUID des Spielers, der den Report erstellt hat oder null falls der Report vom System kommt
"reporter": string | null,
// Grund des Reports
"reason": string,
// Wann der Report abgeschickt wurde als UTC Millisekunden oder null falls der Report noch nicht abgeschickt wurde (=> kann noch bearbeitet werden)
"created": number | null,
// Status des Reports, "open" wenn er gerade bearbeitet wird, "closed" falls er bearbeitet wurde, null wenn nichts von beidem
"status": "open" | "closed" | null,
// Url zum Report auf der Website
"url": string
}[]
}
```
</details>
## Webhook ## Webhook
> Die env variable `WEBHOOK_ENDPOINT` muss gesetzt und eine valide HTTP URL sein. > Die env variable `WEBHOOK_ENDPOINT` muss gesetzt und eine valide HTTP URL sein.

View File

@@ -120,11 +120,13 @@ export async function getReports(db: Database, values: GetReportsReq) {
createdAt: report.createdAt, createdAt: report.createdAt,
reporter: { reporter: {
id: reporter.id, id: reporter.id,
username: reporter.username username: reporter.username,
uuid: reporter.uuid
}, },
reported: { reported: {
id: reported.id, id: reported.id,
username: reported.username username: reported.username,
uuid: reported.uuid
}, },
status: { status: {
status: reportStatus.status, status: reportStatus.status,

34
src/pages/api/_api.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { APIRoute } from 'astro';
import { z } from 'astro:schema';
import { checkApiBasicAuth } from '@util/auth.ts';
export function externalApi<InputSchema extends z.ZodType | undefined>(params: {
input?: InputSchema;
auth?: boolean;
handler: ({
input,
params
}: {
input: InputSchema extends z.ZodType ? z.infer<InputSchema> : {};
params: Record<string, string | undefined>;
}) => Response | Promise<Response>;
}): APIRoute {
return async (context) => {
if (params.auth && !checkApiBasicAuth(context.request.headers)) {
return new Response(null, { status: 401 });
}
let input;
if (params.input) {
try {
input = await params.input.parseAsync(await context.request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
} else {
input = {};
}
return params.handler({ input: input, params: context.params });
};
}

View File

@@ -1,37 +0,0 @@
import { z } from 'astro:schema';
import type { APIRoute } from 'astro';
import { db } from '@db/database.ts';
import { BASE_PATH } from 'astro:env/server';
import { checkApiBasicAuth } from '@util/auth.ts';
const postSchema = z.object({
event: z.string(),
title: z.string(),
uuids: z.array(z.string())
});
export const POST: APIRoute = async ({ request }) => {
if (!checkApiBasicAuth(request.headers)) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await postSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const feedbacks = await db.addUserFeedbacks({
event: parsed.event,
title: parsed.title,
uuids: parsed.uuids
});
const response = feedbacks.map((feedback) => ({
uuid: feedback.uuid,
url: `${BASE_PATH}/feedback/${feedback.urlHash}`
}));
return new Response(JSON.stringify({ feedback: response }), { status: 200 });
};

View File

@@ -0,0 +1,27 @@
import { externalApi } from '../_api.ts';
import { z } from 'astro:schema';
import { db } from '@db/database.ts';
import { BASE_PATH } from 'astro:env/server';
export const POST = externalApi({
input: z.object({
event: z.string(),
title: z.string(),
uuids: z.array(z.string())
}),
auth: true,
handler: async ({ input }) => {
const feedbacks = await db.addUserFeedbacks({
event: input.event,
title: input.title,
uuids: input.uuids
});
const response = feedbacks.map((feedback) => ({
uuid: feedback.uuid,
url: `${BASE_PATH}/feedback/${feedback.urlHash}`
}));
return new Response(JSON.stringify({ feedback: response }), { status: 200 });
}
});

View File

@@ -1,37 +0,0 @@
import { z } from 'astro:schema';
import type { APIRoute } from 'astro';
import { db } from '@db/database.ts';
import { checkApiBasicAuth } from '@util/auth.ts';
const postSchema = z.object({
uuid: z.string()
});
export const POST: APIRoute = async ({ request }) => {
if (!checkApiBasicAuth(request.headers)) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await postSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const user = await db.getUserByUuid({ uuid: parsed.uuid });
if (!user) return new Response(null, { status: 404 });
const strikes = await db.getStrikesByUserId({ userId: user.id });
return new Response(
JSON.stringify({
firstname: user.firstname,
lastname: user.lastname,
username: user.username,
uuid: user.uuid,
strikes: strikes.map((s) => ({ at: s.at.getTime(), weight: s.reason.weight }))
}),
{ status: 200 }
);
};

View File

@@ -1,103 +0,0 @@
import type { APIRoute } from 'astro';
import { z } from 'astro:schema';
import { db } from '@db/database.ts';
import { sendWebhook, WebhookAction } from '@util/webhook.ts';
import { checkApiBasicAuth } from '@util/auth.ts';
const postSchema = z.object({
reporter: z.string(),
reported: z.string().nullable(),
reason: z.string()
});
export const POST: APIRoute = async ({ request }) => {
if (!checkApiBasicAuth(request.headers)) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await postSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const reporter = await db.getUserByUuid({ uuid: parsed.reporter });
if (!reporter) return new Response(null, { status: 404 });
let reported = null;
if (parsed.reported) {
reported = await db.getUserByUuid({ uuid: parsed.reported });
if (!reported) return new Response(null, { status: 404 });
}
const report = await db.addReport({
reporterId: reporter.id,
reportedId: reported?.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 (!checkApiBasicAuth(request.headers)) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await putSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const reported = await db.getUserByUuid({ uuid: parsed.reported });
if (!reported) return new Response(null, { status: 404 });
let reporter = null;
if (parsed.reporter) {
reporter = await db.getUserByUuid({ uuid: parsed.reporter });
if (!reporter) return new Response(null, { status: 404 });
}
await db.transaction(async (tx) => {
const report = await tx.addReport({
reporterId: reporter?.id,
reportedId: reported.id,
createdAt: new Date(),
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
});
});
// send webhook in background
sendWebhook(WebhookAction.Strike, {
uuid: reported.uuid!
});
return new Response(null, { status: 200 });
};

View File

@@ -0,0 +1,84 @@
import { externalApi } from '../_api.ts';
import { z } from 'astro:schema';
import { db } from '@db/database.ts';
import { sendWebhook, WebhookAction } from '@util/webhook.ts';
export const POST = externalApi({
input: z.object({
reporter: z.string(),
reported: z.string().nullable(),
reason: z.string()
}),
auth: true,
handler: async ({ input }) => {
const reporter = await db.getUserByUuid({ uuid: input.reporter });
if (!reporter) return new Response(null, { status: 404 });
let reported = null;
if (input.reported) {
reported = await db.getUserByUuid({ uuid: input.reported });
if (!reported) return new Response(null, { status: 404 });
}
const report = await db.addReport({
reporterId: reporter.id,
reportedId: reported?.id,
reason: input.reason,
body: null
});
return new Response(JSON.stringify({ url: report.url }), { status: 200 });
}
});
export const PUT = externalApi({
input: 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()
}),
auth: true,
handler: async ({ input }) => {
const reported = await db.getUserByUuid({ uuid: input.reported });
if (!reported) return new Response(null, { status: 404 });
let reporter = null;
if (input.reporter) {
reporter = await db.getUserByUuid({ uuid: input.reporter });
if (!reporter) return new Response(null, { status: 404 });
}
await db.transaction(async (tx) => {
const report = await tx.addReport({
reporterId: reporter?.id,
reportedId: reported.id,
createdAt: new Date(),
reason: input.reason,
body: input.body
});
await tx.editReportStatus({
reportId: report.id,
notice: input.notice,
statement: input.statement,
status: 'closed'
});
await tx.editStrike({
reportId: report.id,
strikeReasonId: input.strike_reason_id
});
});
// send webhook in background
sendWebhook(WebhookAction.Strike, {
uuid: reported.uuid!
});
return new Response(null, { status: 200 });
}
});

View File

@@ -0,0 +1,23 @@
import { externalApi } from '../../_api.ts';
import { db } from '@db/database.ts';
export const GET = externalApi({
auth: true,
handler: async ({ params }) => {
const user = await db.getUserByUuid({ uuid: params['uuid']! });
if (!user) return new Response(null, { status: 404 });
const strikes = await db.getStrikesByUserId({ userId: user.id });
return new Response(
JSON.stringify({
firstname: user.firstname,
lastname: user.lastname,
username: user.username,
uuid: user.uuid,
strikes: strikes.map((s) => ({ at: s.at.getTime(), weight: s.reason.weight }))
}),
{ status: 200 }
);
}
});

View File

@@ -0,0 +1,34 @@
import { externalApi } from '../../_api.ts';
import { db } from '@db/database.ts';
import { BASE_PATH } from 'astro:env/server';
export const GET = externalApi({
auth: true,
handler: async ({ params }) => {
const user = await db.getUserByUuid({ uuid: params['uuid']! });
if (!user) return new Response(null, { status: 404 });
const fromSelf = await db.getReports({ reporter: user.username });
const toSelf = await db.getReports({ reported: user.username });
return new Response(
JSON.stringify({
from_self: fromSelf.map((report) => ({
reported: report.reported?.uuid ?? null,
reason: report.reason,
created: report.createdAt?.getTime() ?? null,
status: report.status?.status ?? null,
url: `${BASE_PATH}/report/${report.urlHash}`
})),
to_self: toSelf.map((report) => ({
reporter: report.reporter?.uuid ?? null,
reason: report.reason,
created: report.createdAt,
status: report.status?.status ?? null,
url: `${BASE_PATH}/report/${report.urlHash}`
}))
}),
{ status: 200 }
);
}
});