refactor(server): parse fields from x-www-form-urlencoded request

This commit is contained in:
Diogo Correia 2026-07-19 14:59:27 +01:00
parent 0dd39fbb0e
commit 4c1eb53aaf
No known key found for this signature in database
GPG key ID: 12B4F3AC9C065D08
8 changed files with 36 additions and 40 deletions

View file

@ -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:

View file

@ -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' });

View file

@ -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' });

View file

@ -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<any> {
const req = context.switchToHttp().getRequest<Request>();
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();
}
}

View file

@ -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() {

View file

@ -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();

View file

@ -102,13 +102,12 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
}
const url = getBaseUrl() + '/download/archive' + (queryParams ? `?${queryParams}` : '');
const payload = { assetIds: archive.assetIds, edited: true, archiveName };
try {
if (downloadInfo.archives.length > 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(

View file

@ -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);
};