From 4c1eb53aaf92726bd04bb1ec06788096431a9869 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 19 Jul 2026 14:59:27 +0100 Subject: [PATCH] refactor(server): parse fields from x-www-form-urlencoded request --- server/src/controllers/download.controller.ts | 4 +--- server/src/dtos/asset.dto.ts | 2 +- server/src/dtos/download.dto.ts | 13 ++++++++++- .../middleware/forms-to-json.interceptor.ts | 22 ------------------- .../lib/managers/download-manager.svelte.ts | 7 +++--- web/src/lib/utils.ts | 21 +++++++++++++----- web/src/lib/utils/asset-utils.ts | 5 ++--- web/src/routes/DownloadPanel.svelte | 2 +- 8 files changed, 36 insertions(+), 40 deletions(-) delete mode 100644 server/src/middleware/forms-to-json.interceptor.ts diff --git a/server/src/controllers/download.controller.ts b/server/src/controllers/download.controller.ts index 593f72b241..2bdf5c4837 100644 --- a/server/src/controllers/download.controller.ts +++ b/server/src/controllers/download.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, HttpCode, HttpStatus, Post, Res, StreamableFile, UseInterceptors } from '@nestjs/common'; +import { Body, Controller, HttpCode, HttpStatus, Post, Res, StreamableFile } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; @@ -6,7 +6,6 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadArchiveDto, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; -import { FormsToJsonInterceptor } from 'src/middleware/forms-to-json.interceptor'; import { DownloadService } from 'src/services/download.service'; import { asStreamableFile } from 'src/utils/file'; @@ -31,7 +30,6 @@ export class DownloadController { @Authenticated({ permission: Permission.AssetDownload, sharedLink: true }) @FileResponse() @HttpCode(HttpStatus.OK) - @UseInterceptors(FormsToJsonInterceptor) @Endpoint({ summary: 'Download asset archive', description: diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 26c983a7de..4053ad0d6b 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -59,7 +59,7 @@ const AssetBulkDeleteSchema = BulkIdsSchema.extend({ export const AssetIdsSchema = z .object({ - assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + assetIds: z.preprocess((val) => (typeof val === 'string' ? [val] : val), z.array(z.uuidv4())).describe('Asset IDs'), }) .meta({ id: 'AssetIdsDto' }); diff --git a/server/src/dtos/download.dto.ts b/server/src/dtos/download.dto.ts index 5f504449c4..8fe33b447a 100644 --- a/server/src/dtos/download.dto.ts +++ b/server/src/dtos/download.dto.ts @@ -26,7 +26,18 @@ const DownloadResponseSchema = z .meta({ id: 'DownloadResponseDto' }); const DownloadArchiveSchema = AssetIdsSchema.extend({ - edited: z.boolean().optional().describe('Download edited asset if available'), + edited: z + .preprocess((val) => { + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + return val; + }, z.boolean()) + .optional() + .describe('Download edited asset if available'), archiveName: z.string().optional().describe('The name of the archive to download, without extension'), }).meta({ id: 'DownloadArchiveDto' }); diff --git a/server/src/middleware/forms-to-json.interceptor.ts b/server/src/middleware/forms-to-json.interceptor.ts deleted file mode 100644 index 400e557ad3..0000000000 --- a/server/src/middleware/forms-to-json.interceptor.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { Request } from 'express'; -import { Observable } from 'rxjs'; - -@Injectable() -export class FormsToJsonInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - const req = context.switchToHttp().getRequest(); - - const contentType = req.headers['content-type']; - if (contentType?.startsWith('application/x-www-form-urlencoded')) { - try { - req.body = JSON.parse(req?.body?.json); - req.headers['content-type'] = 'application/json'; - } catch { - // ignore if failed to parse - } - } - - return next.handle(); - } -} diff --git a/web/src/lib/managers/download-manager.svelte.ts b/web/src/lib/managers/download-manager.svelte.ts index 09765e96d4..5ff0dfccb1 100644 --- a/web/src/lib/managers/download-manager.svelte.ts +++ b/web/src/lib/managers/download-manager.svelte.ts @@ -2,7 +2,8 @@ import { SvelteMap } from 'svelte/reactivity'; export interface DownloadState { url: string; - payload: unknown; + assetIds: string[]; + archiveName: string; total: number; downloaded: boolean; } @@ -12,8 +13,8 @@ class DownloadManager { isDownloading = $derived(this.assets.size > 0); - add(key: string, url: string, payload: unknown, total: number) { - this.assets.set(key, { url, payload, total, downloaded: false }); + add(key: string, url: string, assetIds: string[], archiveName: string, total: number) { + this.assets.set(key, { url, assetIds, archiveName, total, downloaded: false }); } clearAll() { diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 7c4813db83..900f15fe26 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -293,17 +293,26 @@ export const downloadUrl = (url: string, filename: string) => { URL.revokeObjectURL(url); }; -export const downloadUrlPost = (url: string, data: unknown) => { +export const downloadUrlPost = (url: string, assetIds: string[], archiveName: string) => { const form = document.createElement('form'); form.method = 'post'; form.action = url; form.target = '_blank'; - const inputJson = document.createElement('input'); - inputJson.type = 'hidden'; - inputJson.name = 'json'; - inputJson.value = JSON.stringify(data); - form.append(inputJson); + function mkInput(name: string, value: string) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + form.append(input); + } + + for (const assetId of assetIds) { + mkInput('assetIds', assetId); + } + + mkInput('archiveName', archiveName); + mkInput('edited', 'true'); document.body.append(form); form.submit(); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index fa20c7e8f0..5f77afc0c3 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -102,13 +102,12 @@ export const downloadArchive = async (fileName: string, options: Omit 1) { - downloadManager.add(downloadKey, url, payload, archive.size); + downloadManager.add(downloadKey, url, archive.assetIds, archiveName, archive.size); } else { - downloadUrlPost(url, payload); + downloadUrlPost(url, archive.assetIds, archiveName); const $t = await getFormatter(); const $locale = get(locale); toastManager.primary( diff --git a/web/src/routes/DownloadPanel.svelte b/web/src/routes/DownloadPanel.svelte index e7cd0bd156..82425ccab2 100644 --- a/web/src/routes/DownloadPanel.svelte +++ b/web/src/routes/DownloadPanel.svelte @@ -9,7 +9,7 @@ import { downloadUrlPost } from '$lib/utils'; const startDownload = (downloadKey: string, download: DownloadState) => { - downloadUrlPost(download.url, download.payload); + downloadUrlPost(download.url, download.assetIds, download.archiveName); downloadManager.markDownloaded(downloadKey); };