This commit is contained in:
michael committed 2026-09-11 10:52:13 +02:00
1 parent 87d6c9b3ce
commit 9a08bfbf44
2 files changed
+47 -12

No files matched your search

+31 -8
View File
@@ -27,9 +27,11 @@ const port = Number(process.env.PORT ?? 3000);
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const frontendDist = path.resolve(__dirname, "../../frontend/dist"); const frontendDist = path.resolve(__dirname, "../../frontend/dist");
const serveFrontend = process.env.SERVE_FRONTEND !== "false"; const serveFrontend = process.env.SERVE_FRONTEND !== "false";
const requestBodyLimit = process.env.REQUEST_BODY_LIMIT ?? "250mb";
app.use(cors()); 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) => { app.get("/api/health", async (_req, res) => {
await query("SELECT 1"); await query("SELECT 1");
@@ -3340,6 +3342,18 @@ app.get("/api/setup/status", async (_req, res) => {
res.json({ required: !(await hasAdminUser()) }); 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) => { app.post("/api/setup/admin", async (req, res) => {
if (await hasAdminUser()) { if (await hasAdminUser()) {
res.status(409).json({ error: "Setup wurde bereits abgeschlossen" }); res.status(409).json({ error: "Setup wurde bereits abgeschlossen" });
@@ -3390,7 +3404,7 @@ app.post("/api/setup/restore", async (req, res) => {
return; return;
} }
const sql = requireString(req.body.sql, "sql"); const sql = restoreSqlFromRequestBody(req.body);
const result = await restoreTicketTrackerBackup(sql); const result = await restoreTicketTrackerBackup(sql);
res.status(201).json(result); res.status(201).json(result);
}); });
@@ -3401,7 +3415,7 @@ app.post("/api/setup/restore/validate", async (req, res) => {
return; return;
} }
const sql = requireString(req.body.sql, "sql"); const sql = restoreSqlFromRequestBody(req.body);
res.json(await validateTicketTrackerBackup(sql)); 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) => { 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)); res.json(await validateTicketTrackerBackup(sql));
}); });
app.post("/api/admin/restore/database", requireAdmin, async (req, res) => { 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); const result = await restoreTicketTrackerBackup(sql);
res.clearCookie("tickettracker_session", { 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); 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"
}); });
}); });
+16 -4
View File
@@ -140,6 +140,10 @@ async function request<T>(url: string, options: ApiRequestInit = {}): Promise<T>
throw new ApiError("Anfrage hat zu lange gedauert", 408); 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; throw error;
} finally { } finally {
if (timeout) { if (timeout) {
@@ -223,14 +227,18 @@ export function createSetupAdmin(payload: { username: string; displayName: strin
export function restoreSetupBackup(sql: string) { export function restoreSetupBackup(sql: string) {
return request<{ ok: boolean }>("/api/setup/restore", { return request<{ ok: boolean }>("/api/setup/restore", {
method: "POST", 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) { export function validateSetupBackup(sql: string) {
return request<RestoreValidationResult>("/api/setup/restore/validate", { return request<RestoreValidationResult>("/api/setup/restore/validate", {
method: "POST", 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) { export function restoreDatabaseBackup(sql: string) {
return request<{ ok: boolean }>("/api/admin/restore/database", { return request<{ ok: boolean }>("/api/admin/restore/database", {
method: "POST", 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) { export function validateDatabaseBackup(sql: string) {
return request<RestoreValidationResult>("/api/admin/restore/database/validate", { return request<RestoreValidationResult>("/api/admin/restore/database/validate", {
method: "POST", method: "POST",
body: JSON.stringify({ sql }) timeoutMs: 10 * 60 * 1000,
headers: { "Content-Type": "text/plain; charset=utf-8" },
body: sql
}); });
} }