From eef8e57f6c4341a3e1fe37d63880b42947b93abf Mon Sep 17 00:00:00 2001 From: Dominic R Date: Tue, 16 Dec 2025 13:37:22 -0500 Subject: [PATCH] web: fix file upload form (#18808) * web: fix file upload form name mismatch and modal submit promise handling Fixes the following error: FileUploadForm.ts:74 POST http://authentik.localhost:9000/api/v3/admin/file/ 405 (Method Not Allowed) (anonymous) @ fetch.ts:81 fetchApi @ runtime.ts:206 await in fetchApi request @ runtime.ts:136 await in request adminFileCreateRaw @ AdminApi.ts:191 adminFileCreate @ AdminApi.ts:206 send @ FileUploadForm.ts:74 submit @ Form.ts:363 (anonymous) @ ModalForm.ts:54 handleEvent @ lit-html.ts:2109 n @ helpers.ts:117Understand this error Form.ts:403 authentik/forms: API rejected the form submission due to an invalid field that doesn't appear to be in the form. This is likely a bug in authentik. {detail: 'Response returned an error code'} (anonymous) @ console.ts:39 (anonymous) @ Form.ts:403 Promise.catch submit @ Form.ts:376 (anonymous) @ ModalForm.ts:54 handleEvent @ lit-html.ts:2109 n @ helpers.ts:117Understand this error runtime.ts:140 Uncaught (in promise) ResponseError: Response returned an error code at mR.request (runtime.ts:140:15) at async mR.adminFileCreateRaw (AdminApi.ts:191:26) at async mR.adminFileCreate (AdminApi.ts:206:9) - align file upload rename field with api name so validation errors map correctly -improve custom filename extension logic to avoid double or incorrect extensions - prevent unhandled promise rejections from modal submit click handler and show missing-form errors to users * rev * wip * Update ModalForm.ts Signed-off-by: Dominic R * scope better * fix what it validates against --------- Signed-off-by: Dominic R --- web/src/admin/files/FileUploadForm.ts | 67 ++++++++++++++++----------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/web/src/admin/files/FileUploadForm.ts b/web/src/admin/files/FileUploadForm.ts index 94430badad..39f94bd11b 100644 --- a/web/src/admin/files/FileUploadForm.ts +++ b/web/src/admin/files/FileUploadForm.ts @@ -16,18 +16,33 @@ import { createRef, ref } from "lit/directives/ref.js"; // Same regex is used in the backend as well const VALID_FILE_NAME_PATTERN = /^[a-zA-Z0-9._/-]+$/; -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source -// This is perfect for the "pattern" attribute -const VALID_FILE_NAME_PATTERN_STRING = VALID_FILE_NAME_PATTERN.source; + +// Note: browsers compile `pattern` using the new `v` RegExp flag (Unicode sets). Under `/v`, +// both `/` and `-` must be escaped inside character classes. +const VALID_FILE_NAME_PATTERN_STRING = "^[a-zA-Z0-9._\\/\\-]+$"; function assertValidFileName(fileName: string): void { if (!VALID_FILE_NAME_PATTERN.test(fileName)) { throw new Error( - msg("Filename can only contain letters, numbers, dots, hyphens, and underscores"), + msg( + "Filename can only contain letters, numbers, dots, hyphens, underscores, and slashes", + ), ); } } +function getFileExtension(fileName: string): string { + const lastDot = fileName.lastIndexOf("."); + if (lastDot <= 0) return ""; + return fileName.slice(lastDot); +} + +function hasBasenameExtension(fileName: string): boolean { + const baseName = fileName.split("/").pop() ?? fileName; + const lastDot = baseName.lastIndexOf("."); + return lastDot > 0; +} + @customElement("ak-file-upload-form") export class FileUploadForm extends Form> { @property({ type: String, useDefault: true }) @@ -57,36 +72,36 @@ export class FileUploadForm extends Form> { throw new PreventFormSubmit("Selected file not provided", this); } - assertValidFileName(this.selectedFile.name); - const api = new AdminApi(DEFAULT_CONFIG); - const customName = typeof data.fileName === "string" ? data.fileName.trim() : ""; + const customName = typeof data.name === "string" ? data.name.trim() : ""; // If custom name provided, validate and append original extension + // Only validate the original filename if no custom name is provided let finalName = this.selectedFile.name; if (customName) { assertValidFileName(customName); - const ext = this.selectedFile.name.substring(this.selectedFile.name.lastIndexOf(".")); - finalName = customName + ext; + const ext = getFileExtension(this.selectedFile.name); + finalName = + ext && !hasBasenameExtension(customName) ? `${customName}${ext}` : customName; + } else { + assertValidFileName(this.selectedFile.name); } - return api - .adminFileCreate({ - file: this.selectedFile, - name: finalName, - usage: this.usage, - }) - .then(() => { - showMessage({ - level: MessageLevel.success, - message: msg("File uploaded successfully"), - }); + assertValidFileName(finalName); - this.reset(); - }) - .finally(() => { - this.clearFileInput(); - }); + await api.adminFileCreate({ + file: this.selectedFile, + name: finalName, + usage: this.usage, + }); + + showMessage({ + level: MessageLevel.success, + message: msg("File uploaded successfully"), + }); + + this.reset(); + this.clearFileInput(); } renderForm() { @@ -101,7 +116,7 @@ export class FileUploadForm extends Form> { @change=${this.#fileChangeListener} /> - +