From 285d812f8e9cf923407fd11bd33db91f7cd9bfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 13:49:32 +0200 Subject: [PATCH 1/8] fix(converters): resolve reference mutation and discovery priority in main.ts --- src/converters/main.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/converters/main.ts b/src/converters/main.ts index 8377011..9c063e1 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -246,8 +246,8 @@ async function mainConverter( converterFunc = properties[converterName]?.converter; } else { // Iterate over each converter in properties - for (converterName in properties) { - const converterObj = properties[converterName]; + for (const name in properties) { + const converterObj = properties[name]; if (!converterObj) { break; @@ -262,6 +262,9 @@ async function mainConverter( break; } } + if (converterFunc) { + break; + } } } @@ -349,7 +352,7 @@ for (const converterName in properties) { if (allTargets[converterName]) { allTargets[converterName].push(...(converterProperties.to[key] || [])); } else { - allTargets[converterName] = converterProperties.to[key] || []; + allTargets[converterName] = [...(converterProperties.to[key] || [])]; } } } From a95effbc835e3e1807d73db1d470ccff60afdb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 13:51:27 +0200 Subject: [PATCH 2/8] fix(converters): improve CSV header generation and FFMPEG argument parsing --- src/converters/ffmpeg.ts | 4 ++-- src/converters/vcf.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index 8d5709a..926b62d 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -770,9 +770,9 @@ export async function convert( } // Parse FFMPEG_ARGS environment variable into array - const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : []; + const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/).filter(Boolean) : []; const ffmpegOutputArgs = process.env.FFMPEG_OUTPUT_ARGS - ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/) + ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/).filter(Boolean) : []; return new Promise((resolve, reject) => { diff --git a/src/converters/vcf.ts b/src/converters/vcf.ts index 2fff8ed..d84094c 100644 --- a/src/converters/vcf.ts +++ b/src/converters/vcf.ts @@ -46,12 +46,14 @@ export function parseVCF(data: string): Record[] { export function toCSV(data: Record[]): string { if (!data.length) return ""; - const first = data[0]; - if (!first) return ""; - const headers = Object.keys(first); + const headers = new Set(); + for (const row of data) { + Object.keys(row).forEach((key) => headers.add(key)); + } + const headerArray = Array.from(headers); const escape = (str: string) => `"${str.replace(/"/g, '""')}"`; - const rows = data.map((row) => headers.map((h) => escape(row[h] || "")).join(",")); - return [headers.join(","), ...rows].join("\n"); + const rows = data.map((row) => headerArray.map((h) => escape(row[h] || "")).join(",")); + return [headerArray.join(","), ...rows].join("\n"); } export async function convert( From cc1689a2efeed37d62756f9900adcf74ac7e6397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 13:51:53 +0200 Subject: [PATCH 3/8] fix(pages): improve request handling, session persistence, and file operations --- src/pages/convert.tsx | 13 ++++++--- src/pages/deleteFile.tsx | 10 ++++++- src/pages/download.tsx | 4 +-- src/pages/root.tsx | 58 +++++++++++++++++++++++++--------------- 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index ed367b2..c181e51 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -56,16 +56,21 @@ export const convert = new Elysia().use(userService).post( return redirect(`${WEBROOT}/`, 302); } - const fileNames = JSON.parse(body.file_names) as string[]; - - for (let i = 0; i < fileNames.length; i++) { - fileNames[i] = sanitize(fileNames[i] || ""); + let fileNames: string[]; + try { + fileNames = JSON.parse(body.file_names) as string[]; + } catch (e) { + return redirect(`${WEBROOT}/`, 302); } if (!Array.isArray(fileNames) || fileNames.length === 0) { return redirect(`${WEBROOT}/`, 302); } + for (let i = 0; i < fileNames.length; i++) { + fileNames[i] = sanitize(fileNames[i] || ""); + } + db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run( fileNames.length, jobId.value, diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index e18caef..a43fd59 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -27,7 +27,15 @@ export const deleteFile = new Elysia().use(userService).post( const sanitized = sanitize(body.filename); const targetPath = path.join(userUploadsDir, sanitized); - await unlink(targetPath); + try { + await unlink(targetPath); + } catch (error) { + if (error.code === "ENOENT") { + return { message: "File not found." }; + } + console.error(`Failed to unlink ${targetPath}:`, error); + throw error; + } return { message: "File deleted successfully.", diff --git a/src/pages/download.tsx b/src/pages/download.tsx index ffc64e9..f4dc7cf 100644 --- a/src/pages/download.tsx +++ b/src/pages/download.tsx @@ -21,8 +21,8 @@ export const download = new Elysia() return redirect(`${WEBROOT}/results`, 302); } // parse from URL encoded string - const jobId = decodeURIComponent(params.jobId); - const fileName = sanitize(decodeURIComponent(params.fileName)); + const jobId = params.jobId; + const fileName = sanitize(params.fileName); const filePath = `${outputDir}${userId}/${jobId}/${fileName}`; const file = Bun.file(filePath); diff --git a/src/pages/root.tsx b/src/pages/root.tsx index eba3238..b0ea02f 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -33,30 +33,44 @@ export const root = new Elysia().use(userService).get( // validate jwt let user: ({ id: string } & JWTPayloadSpec) | false = false; if (ALLOW_UNAUTHENTICATED) { - const newUserId = String( - UNAUTHENTICATED_USER_SHARING - ? 0 - : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), - ); - const accessToken = await jwt.sign({ - id: newUserId, - }); - - user = { id: newUserId }; - if (!auth) { - return { - message: "No auth cookie, perhaps your browser is blocking cookies.", - }; + // Check if the current cookie is already a valid session + if (auth?.value) { + try { + const verifiedUser = await jwt.verify(auth.value); + if (verifiedUser && verifiedUser.id) { + user = verifiedUser; + } + } catch (e) { + // Invalid cookie, fall through to anonymous creation + } } - // set cookie - auth.set({ - value: accessToken, - httpOnly: true, - secure: !HTTP_ALLOWED, - maxAge: 24 * 60 * 60, - sameSite: "strict", - }); + // If no valid session found, create an anonymous user + if (!user) { + const newUserId = String( + UNAUTHENTICATED_USER_SHARING + ? 0 + : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), + ); + const accessToken = await jwt.sign({ + id: newUserId, + }); + + user = { id: newUserId }; + if (!auth) { + return { + message: "No auth cookie, perhaps your browser is blocking cookies.", + }; + } + + auth.set({ + value: accessToken, + httpOnly: true, + secure: !HTTP_ALLOWED, + maxAge: 24 * 60 * 60, + sameSite: "strict", + }); + } } else if (auth?.value) { user = await jwt.verify(auth.value); From 5cdce6b4b4ed3772650d5b754edcd144a3f57015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 16:55:41 +0200 Subject: [PATCH 4/8] fix(converters): fix converter discovery and array sharing bugs --- src/converters/main.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/converters/main.ts b/src/converters/main.ts index 9c063e1..6abedfd 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -259,6 +259,7 @@ async function mainConverter( converterObj?.properties?.to[key]?.includes(convertTo) ) { converterFunc = converterObj.converter; + converterName = name; break; } } @@ -299,6 +300,7 @@ const possibleTargets: Record> = {}; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; + if (!converterProperties) continue; for (const key in converterProperties.from) { @@ -310,7 +312,7 @@ for (const converterName in properties) { for (const ext of fromList) { if (!possibleTargets[ext]) possibleTargets[ext] = {}; - possibleTargets[ext][converterName] = toList; + possibleTargets[ext][converterName] = [...toList]; } } } @@ -319,15 +321,13 @@ export const getPossibleTargets = (from: string): Record => { const fromClean = normalizeFiletype(from); return possibleTargets[fromClean] || {}; -}; +} const possibleInputs: string[] = []; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; - if (!converterProperties) { - continue; - } + if (!converterProperties) continue; for (const key in converterProperties.from) { for (const extension of converterProperties.from[key] ?? []) { @@ -344,13 +344,11 @@ const allTargets: Record = {}; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; - if (!converterProperties) { - continue; - } + if (!converterProperties) continue; for (const key in converterProperties.to) { if (allTargets[converterName]) { - allTargets[converterName].push(...(converterProperties.to[key] || [])); + allTargets[converterName] = [...allTargets[converterName], ...(converterProperties.to[key] || [])]; } else { allTargets[converterName] = [...(converterProperties.to[key] || [])]; } @@ -365,13 +363,11 @@ const allInputs: Record = {}; for (const converterName in properties) { const converterProperties = properties[converterName]?.properties; - if (!converterProperties) { - continue; - } + if (!converterProperties) continue; for (const key in converterProperties.from) { if (allInputs[converterName]) { - allInputs[converterName].push(...(converterProperties.from[key] || [])); + allInputs[converterName] = [...allInputs[converterName], ...(converterProperties.from[key] || [])]; } else { allInputs[converterName] = converterProperties.from[key] || []; } @@ -386,4 +382,4 @@ export const getAllInputs = (converter: string) => { * @internal For testing only. Do not use in production. * Tests need direct access to cover all branches of converter discovery and chunking logic. */ -export { chunks, mainConverter }; +export { chunks, mainConverter }; \ No newline at end of file From e17f29552c0dfbe09a9bbfcc9c914535ad923be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 16:56:13 +0200 Subject: [PATCH 5/8] fix(pages): fix session preservation on unauthenticated route --- src/pages/root.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pages/root.tsx b/src/pages/root.tsx index b0ea02f..4aacd25 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -57,19 +57,21 @@ export const root = new Elysia().use(userService).get( }); user = { id: newUserId }; - if (!auth) { + + // Use existing cookie if it exists, otherwise set a new one + if (auth) { + auth.set({ + value: accessToken, + httpOnly: true, + secure: !HTTP_ALLOWED, + maxAge: 24 * 60 * 60, + sameSite: "strict", + }); + } else { return { message: "No auth cookie, perhaps your browser is blocking cookies.", }; } - - auth.set({ - value: accessToken, - httpOnly: true, - secure: !HTTP_ALLOWED, - maxAge: 24 * 60 * 60, - sameSite: "strict", - }); } } else if (auth?.value) { user = await jwt.verify(auth.value); From f570d53ad48cd77afee206e0b41cc903c8481420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 16:56:38 +0200 Subject: [PATCH 6/8] fix(tests): remove failing annotations after bugfixes --- tests/converters/ffmpeg.test.ts | 5 +---- tests/converters/main.test.ts | 17 +++-------------- tests/converters/vcf.test.ts | 5 +---- tests/pages/convert.test.ts | 5 +---- tests/pages/deleteFile.test.ts | 6 +----- tests/pages/download.test.ts | 6 +----- tests/pages/root.test.ts | 7 +------ 7 files changed, 9 insertions(+), 42 deletions(-) diff --git a/tests/converters/ffmpeg.test.ts b/tests/converters/ffmpeg.test.ts index 955e2a0..54c1b21 100644 --- a/tests/converters/ffmpeg.test.ts +++ b/tests/converters/ffmpeg.test.ts @@ -181,10 +181,7 @@ test("splits FFMPEG_ARGS on any whitespace", async () => { expect(calls[0]?.slice(0, 3)).toEqual(["-hide_banner", "-y", "-nostdin"]); }); -// BUG: split(/\s+/) keeps empty strings for leading/trailing whitespace -// (" -y ".split(/\s+/) is ["", "-y", ""]), so ffmpeg receives "" as an argument -// and fails. Remove `.failing` once src/converters/ffmpeg.ts drops empty entries. -test.failing("ignores leading and trailing whitespace in FFMPEG_ARGS", async () => { +test("ignores leading and trailing whitespace in FFMPEG_ARGS", async () => { process.env.FFMPEG_ARGS = " -y "; process.env.FFMPEG_OUTPUT_ARGS = " -preset veryfast "; diff --git a/tests/converters/main.test.ts b/tests/converters/main.test.ts index e8d7f93..96e05e5 100644 --- a/tests/converters/main.test.ts +++ b/tests/converters/main.test.ts @@ -148,11 +148,7 @@ describe("converter registry", () => { expect(getAllInputs("doesnotexist")).toEqual([]); }); - // BUG: building allTargets/allInputs stores the first category's array by - // reference and then pushes the other categories into it. That mutates the - // converters' own properties, so e.g. LibreOffice offers docx -> xlsx and - // libjxl offers jxl -> jxl. Remove `.failing` once main.ts copies the arrays. - test.failing("listing all targets and inputs does not leak formats across categories", () => { + test("listing all targets and inputs does not leak formats across categories", () => { expect(getPossibleTargets("docx").libreoffice).not.toContain("xlsx"); expect(getPossibleTargets("jxl").libjxl).not.toContain("jxl"); expect(libreofficeProperties.from.text).not.toContain("xlsx"); @@ -251,10 +247,7 @@ describe("mainConverter", () => { }, ); - // BUG: discovery loops with `for (converterName in properties)` and only breaks - // the inner loop, so the variable always ends on the last registered converter. - // Remove `.failing` once mainConverter stops at the first match. - test.failing("logs the name of the auto-discovered converter", async () => { + test("logs the name of the auto-discovered converter", async () => { const inputPath = `${uploadsDir}named.vcf`; await writeFile(inputPath, vcard("Named")); @@ -263,11 +256,7 @@ describe("mainConverter", () => { expect(output.logs.join("\n")).toContain("successfully using vcf"); }); - // BUG: same root cause - later matches overwrite earlier ones, so emf -> png - // uses ImageMagick even though Inkscape is registered first on purpose - // ("Prioritize Inkscape for EMF files"). The input does not exist, so whichever - // converter is picked fails fast; only the logged converter name matters. - test.failing("auto-discovery prefers the first registered converter", async () => { + test("auto-discovery prefers the first registered converter", async () => { await mainConverter(`${uploadsDir}missing.emf`, "emf", "png", `${outputDir}missing.png`); expect(output.errors.join("\n")).toContain("using inkscape"); diff --git a/tests/converters/vcf.test.ts b/tests/converters/vcf.test.ts index 06f8953..e4d7f57 100644 --- a/tests/converters/vcf.test.ts +++ b/tests/converters/vcf.test.ts @@ -181,10 +181,7 @@ describe("toCSV", () => { expect(toCSV(contacts)).toBe('Full Name,Email\n"John Doe","john@example.com"\n"Jane Smith",""'); }); - // BUG: the header is built from the first contact only, so fields that only - // later contacts have are silently dropped from the export. Remove `.failing` - // once toCSV builds the header from all contacts. - test.failing("should not drop fields that only later contacts have", () => { + test("should not drop fields that only later contacts have", () => { const contacts = [{ "Full Name": "John Doe" }, { "Full Name": "Jane Smith", Email: "j@x.com" }]; expect(toCSV(contacts)).toContain("j@x.com"); diff --git a/tests/pages/convert.test.ts b/tests/pages/convert.test.ts index 44756a6..ddcb400 100644 --- a/tests/pages/convert.test.ts +++ b/tests/pages/convert.test.ts @@ -154,10 +154,7 @@ describe("POST /convert", () => { ]); }); - // BUG: file_names is parsed and indexed before it is validated, so a value that is not a - // JSON array ends in a 500 instead of the redirect used for every other invalid input. - // Remove `.failing` once convert.tsx validates the parsed value first. - test.failing.each([ + test.each([ ["is not JSON", "not json"], ["is a JSON string", '"a.vcf"'], ])("redirects home when file_names %s", async (_, fileNames) => { diff --git a/tests/pages/deleteFile.test.ts b/tests/pages/deleteFile.test.ts index 1da8f61..5fe8c54 100644 --- a/tests/pages/deleteFile.test.ts +++ b/tests/pages/deleteFile.test.ts @@ -79,11 +79,7 @@ describe("POST /delete", () => { expect(existsSync(`${other.dirs.uploads}keep.txt`)).toBe(true); }); - // BUG: unlink errors are not handled, so deleting a file that does not exist answers - // with a 500 whose body contains the absolute upload path on the server. This also - // happens for every sanitized traversal attempt. Remove `.failing` once deleteFile.tsx - // handles the missing file. - test.failing.each(["missing.txt", "../../1/1/keep.txt"])( + test.each(["missing.txt", "../../1/1/keep.txt"])( "answers a missing file (%s) without a server error or server paths", async (filename) => { const { user, job } = await userWithUploads(); diff --git a/tests/pages/download.test.ts b/tests/pages/download.test.ts index 9827b8e..f6a2e75 100644 --- a/tests/pages/download.test.ts +++ b/tests/pages/download.test.ts @@ -111,11 +111,7 @@ describe("GET /download/:userId/:jobId/:fileName", () => { expect(response.status).toBe(404); }); - // BUG: Elysia already decodes path parameters and download.tsx decodes them a second - // time, so file names containing "%" fail with a 500 "URI error" (and a literal "%20" - // would be turned into a space). The results page links to such files. Remove - // `.failing` once the second decodeURIComponent is dropped. - test.failing("serves file names containing a percent sign", async () => { + test("serves file names containing a percent sign", async () => { const { user, job } = await userWithOutputs({ "100%.csv": "percent" }); const response = await request(`/download/${user.id}/${job}/100%25.csv`, { diff --git a/tests/pages/root.test.ts b/tests/pages/root.test.ts index 9fcc11a..b4f39b3 100644 --- a/tests/pages/root.test.ts +++ b/tests/pages/root.test.ts @@ -142,12 +142,7 @@ describe("GET / without accounts (ALLOW_UNAUTHENTICATED)", () => { expect(body).not.toContain('href="/logoff"'); }); - // BUG: the ALLOW_UNAUTHENTICATED branch runs before the session check, so every visit - // replaces the existing session (even a registered account's) with a new anonymous id - // and the user loses access to their previous jobs. This regressed the fix for #114 - // ("keep unauthenticated user logged in if allowed", bc4ad49) in 538c5b6. Remove - // `.failing` once root.tsx verifies an existing session first. - test.failing("keeps an existing session", async () => { + test("keeps an existing session", async () => { setEnv({ ALLOW_UNAUTHENTICATED: true }); const user = await createUser(); From f7976fa55da164447f81abdc6a231bc20eb5cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 18:10:23 +0200 Subject: [PATCH 7/8] fix(linting): fixed some linting and prettier issues --- src/converters/ffmpeg.ts | 10 ++++++++-- src/converters/main.ts | 14 ++++++++++---- src/pages/convert.tsx | 2 +- src/pages/deleteFile.tsx | 7 +++---- src/pages/root.tsx | 2 +- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/converters/ffmpeg.ts b/src/converters/ffmpeg.ts index 926b62d..adf48b4 100644 --- a/src/converters/ffmpeg.ts +++ b/src/converters/ffmpeg.ts @@ -770,9 +770,15 @@ export async function convert( } // Parse FFMPEG_ARGS environment variable into array - const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/).filter(Boolean) : []; + const ffmpegArgs = process.env.FFMPEG_ARGS + ? process.env.FFMPEG_ARGS.split(/\s+/) + .filter(Boolean) + .map((arg) => arg.trim()) + : []; const ffmpegOutputArgs = process.env.FFMPEG_OUTPUT_ARGS - ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/).filter(Boolean) + ? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/) + .filter(Boolean) + .map((arg) => arg.trim()) : []; return new Promise((resolve, reject) => { diff --git a/src/converters/main.ts b/src/converters/main.ts index 6abedfd..2c8e6b8 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -321,7 +321,7 @@ export const getPossibleTargets = (from: string): Record => { const fromClean = normalizeFiletype(from); return possibleTargets[fromClean] || {}; -} +}; const possibleInputs: string[] = []; for (const converterName in properties) { @@ -348,7 +348,10 @@ for (const converterName in properties) { for (const key in converterProperties.to) { if (allTargets[converterName]) { - allTargets[converterName] = [...allTargets[converterName], ...(converterProperties.to[key] || [])]; + allTargets[converterName] = [ + ...allTargets[converterName], + ...(converterProperties.to[key] || []), + ]; } else { allTargets[converterName] = [...(converterProperties.to[key] || [])]; } @@ -367,7 +370,10 @@ for (const converterName in properties) { for (const key in converterProperties.from) { if (allInputs[converterName]) { - allInputs[converterName] = [...allInputs[converterName], ...(converterProperties.from[key] || [])]; + allInputs[converterName] = [ + ...allInputs[converterName], + ...(converterProperties.from[key] || []), + ]; } else { allInputs[converterName] = converterProperties.from[key] || []; } @@ -382,4 +388,4 @@ export const getAllInputs = (converter: string) => { * @internal For testing only. Do not use in production. * Tests need direct access to cover all branches of converter discovery and chunking logic. */ -export { chunks, mainConverter }; \ No newline at end of file +export { chunks, mainConverter }; diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index c181e51..e28890c 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -59,7 +59,7 @@ export const convert = new Elysia().use(userService).post( let fileNames: string[]; try { fileNames = JSON.parse(body.file_names) as string[]; - } catch (e) { + } catch { return redirect(`${WEBROOT}/`, 302); } diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index a43fd59..ed35550 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -29,12 +29,11 @@ export const deleteFile = new Elysia().use(userService).post( try { await unlink(targetPath); - } catch (error) { - if (error.code === "ENOENT") { + } catch (e) { + if ((e as NodeJS.ErrnoException).code === "ENOENT") { return { message: "File not found." }; } - console.error(`Failed to unlink ${targetPath}:`, error); - throw error; + throw e; } return { diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 4aacd25..99b1f5a 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -40,7 +40,7 @@ export const root = new Elysia().use(userService).get( if (verifiedUser && verifiedUser.id) { user = verifiedUser; } - } catch (e) { + } catch { // Invalid cookie, fall through to anonymous creation } } From eb5e484042823dfcd44ac2cc410b19bb380a07db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Krzeslak?= Date: Thu, 22 Oct 2026 19:08:32 +0200 Subject: [PATCH 8/8] fix(review): fix issues based on review comments --- src/converters/main.ts | 9 +++++---- src/pages/convert.tsx | 4 ++++ src/pages/deleteFile.tsx | 3 ++- src/pages/root.tsx | 10 +++++++++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/converters/main.ts b/src/converters/main.ts index 2c8e6b8..9a74a12 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -85,6 +85,11 @@ const properties: Record< properties: propertiesxelatex, converter: convertxelatex, }, + // djvu before calibre so the dedicated ddjvu converter wins for DjVu files + djvu: { + properties: propertiesDjvu, + converter: convertDjvu, + }, calibre: { properties: propertiesCalibre, converter: convertCalibre, @@ -93,10 +98,6 @@ const properties: Record< properties: propertiesDasel, converter: convertDasel, }, - djvu: { - properties: propertiesDjvu, - converter: convertDjvu, - }, libreoffice: { properties: propertiesLibreOffice, converter: convertLibreOffice, diff --git a/src/pages/convert.tsx b/src/pages/convert.tsx index e28890c..27d7c0e 100644 --- a/src/pages/convert.tsx +++ b/src/pages/convert.tsx @@ -67,6 +67,10 @@ export const convert = new Elysia().use(userService).post( return redirect(`${WEBROOT}/`, 302); } + if (!fileNames.every((name) => typeof name === "string")) { + return redirect(`${WEBROOT}/`, 302); + } + for (let i = 0; i < fileNames.length; i++) { fileNames[i] = sanitize(fileNames[i] || ""); } diff --git a/src/pages/deleteFile.tsx b/src/pages/deleteFile.tsx index ed35550..6302db9 100644 --- a/src/pages/deleteFile.tsx +++ b/src/pages/deleteFile.tsx @@ -9,7 +9,7 @@ import path from "node:path"; export const deleteFile = new Elysia().use(userService).post( "/delete", - async ({ body, redirect, cookie: { jobId }, user }) => { + async ({ body, redirect, set, cookie: { jobId }, user }) => { if (!jobId?.value) { return redirect(`${WEBROOT}/`, 302); } @@ -31,6 +31,7 @@ export const deleteFile = new Elysia().use(userService).post( await unlink(targetPath); } catch (e) { if ((e as NodeJS.ErrnoException).code === "ENOENT") { + set.status = 404; return { message: "File not found." }; } throw e; diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 99b1f5a..0f85f5c 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -38,7 +38,15 @@ export const root = new Elysia().use(userService).get( try { const verifiedUser = await jwt.verify(auth.value); if (verifiedUser && verifiedUser.id) { - user = verifiedUser; + const numericId = Number.parseInt(verifiedUser.id); + // IDs below the anonymous range (excluding shared 0) are registered users + const isRegistered = numericId > 0 && numericId < 2 ** 24; + if ( + !isRegistered || + db.query("SELECT * FROM users WHERE id = ?").as(User).get(verifiedUser.id) + ) { + user = verifiedUser; + } } } catch { // Invalid cookie, fall through to anonymous creation