From 9a08bfbf448469ee92bb6f8782c653167b0b4d65 Mon Sep 17 00:00:00 2001 From: mboehmlaender Date: Fri, 11 Sep 2026 10:52:13 +0200 Subject: [PATCH] Update --- backend/src/index.ts | 39 +++++++++++++++++++++++++++++++-------- frontend/src/api.ts | 20 ++++++++++++++++---- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 23ed756..d24350f 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -27,9 +27,11 @@ const port = Number(process.env.PORT ?? 3000); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const frontendDist = path.resolve(__dirname, "../../frontend/dist"); const serveFrontend = process.env.SERVE_FRONTEND !== "false"; +const requestBodyLimit = process.env.REQUEST_BODY_LIMIT ?? "250mb"; app.use(cors()); -app.use(express.json({ limit: "50mb" })); +app.use(express.text({ type: ["text/plain", "text/sql"], limit: requestBodyLimit })); +app.use(express.json({ limit: requestBodyLimit })); app.get("/api/health", async (_req, res) => { await query("SELECT 1"); @@ -3340,6 +3342,18 @@ app.get("/api/setup/status", async (_req, res) => { res.json({ required: !(await hasAdminUser()) }); }); +function restoreSqlFromRequestBody(body: unknown) { + if (typeof body === "string") { + return requireString(body, "sql"); + } + + if (isPlainObject(body)) { + return requireString(body.sql, "sql"); + } + + throw badRequest("sql is required"); +} + app.post("/api/setup/admin", async (req, res) => { if (await hasAdminUser()) { res.status(409).json({ error: "Setup wurde bereits abgeschlossen" }); @@ -3390,7 +3404,7 @@ app.post("/api/setup/restore", async (req, res) => { return; } - const sql = requireString(req.body.sql, "sql"); + const sql = restoreSqlFromRequestBody(req.body); const result = await restoreTicketTrackerBackup(sql); res.status(201).json(result); }); @@ -3401,7 +3415,7 @@ app.post("/api/setup/restore/validate", async (req, res) => { return; } - const sql = requireString(req.body.sql, "sql"); + const sql = restoreSqlFromRequestBody(req.body); res.json(await validateTicketTrackerBackup(sql)); }); @@ -4157,12 +4171,12 @@ app.get("/api/admin/export/database", requireAdmin, async (_req, res) => { }); app.post("/api/admin/restore/database/validate", requireAdmin, async (req, res) => { - const sql = requireString(req.body.sql, "sql"); + const sql = restoreSqlFromRequestBody(req.body); res.json(await validateTicketTrackerBackup(sql)); }); app.post("/api/admin/restore/database", requireAdmin, async (req, res) => { - const sql = requireString(req.body.sql, "sql"); + const sql = restoreSqlFromRequestBody(req.body); const result = await restoreTicketTrackerBackup(sql); res.clearCookie("tickettracker_session", { @@ -7781,10 +7795,19 @@ if (serveFrontend) { }); } -app.use((error: Error & { status?: number }, _req: express.Request, res: express.Response, _next: express.NextFunction) => { +app.use((error: Error & { status?: number; statusCode?: number; type?: string }, _req: express.Request, res: express.Response, _next: express.NextFunction) => { console.error(error); - res.status(error.status ?? 500).json({ - error: error.status ? error.message : "Internal server error" + + if (error.type === "entity.too.large" || error.status === 413 || error.statusCode === 413) { + res.status(413).json({ + error: `Upload ist zu groß. Aktuelles Limit: ${requestBodyLimit}.` + }); + return; + } + + const status = error.status ?? error.statusCode ?? 500; + res.status(status).json({ + error: status < 500 ? error.message : "Internal server error" }); }); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 5cbb568..9a47e00 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -140,6 +140,10 @@ async function request(url: string, options: ApiRequestInit = {}): Promise throw new ApiError("Anfrage hat zu lange gedauert", 408); } + if (error instanceof TypeError) { + throw new ApiError("Verbindung zum Server wurde unterbrochen. Bitte prüfen, ob Backend und Frontend laufen und ob das Backup kleiner als das Upload-Limit ist.", 503); + } + throw error; } finally { if (timeout) { @@ -223,14 +227,18 @@ export function createSetupAdmin(payload: { username: string; displayName: strin export function restoreSetupBackup(sql: string) { return request<{ ok: boolean }>("/api/setup/restore", { method: "POST", - body: JSON.stringify({ sql }) + timeoutMs: 10 * 60 * 1000, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + body: sql }); } export function validateSetupBackup(sql: string) { return request("/api/setup/restore/validate", { method: "POST", - body: JSON.stringify({ sql }) + timeoutMs: 10 * 60 * 1000, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + body: sql }); } @@ -287,14 +295,18 @@ export async function downloadDatabaseExport() { export function restoreDatabaseBackup(sql: string) { return request<{ ok: boolean }>("/api/admin/restore/database", { method: "POST", - body: JSON.stringify({ sql }) + timeoutMs: 10 * 60 * 1000, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + body: sql }); } export function validateDatabaseBackup(sql: string) { return request("/api/admin/restore/database/validate", { method: "POST", - body: JSON.stringify({ sql }) + timeoutMs: 10 * 60 * 1000, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + body: sql }); }