From c67447d4dbfff656fb8c8366ed38be5d8ae30ca4 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 22 Jan 2026 19:39:10 -0500 Subject: [PATCH] web/admin: fix file upload not preserving extension for custom names with dots (#19548) * web/admin: fix file upload not preserving extension for custom names with dots Overview: The `hasBasenameExtension()` function in `FileUploadForm.ts` incorrectly determined whether a custom filename already had an extension by checking if it contained any dot at position > 0. This caused filenames like "e._.e" to be treated as having an extension, so the original file's extension was not appended. The file would be saved as "e._.e" instead of "e._.e.jpg", which caused `mimetypes.guess_type()` to return `None` (since ".e" is not a recognized extension) and the backend to fall back to "application/octet-stream". Removed `hasBasenameExtension()` entirely. Since the UI explicitly states "Optionally rename the file (without extension)", we now always append the original file's extension when a custom name is provided. Testing: 1. Upload a JPG file with custom name "e" --> saves as "e.jpg", and is detected as "image/jpeg" 2. Upload a JPG file with custom name "e._.e" --> now saves as "e._.e.jpg",and is detected as "image/jpeg" Motivation: Fixes incorrect MIME type detection for uploaded files when users provide custom filenames containing dots. * web: lint * web: Ken's suggestion --- schema.yml | 83 ++++++++----------- web/src/admin/files/FileUploadForm.ts | 21 +---- .../admin/users/UserBulkRevokeSessionsForm.ts | 2 +- 3 files changed, 40 insertions(+), 66 deletions(-) diff --git a/schema.yml b/schema.yml index 83460a3f21..f3a6541071 100644 --- a/schema.yml +++ b/schema.yml @@ -3013,34 +3013,6 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' - /core/authenticated_sessions/bulk_delete/: - delete: - operationId: core_authenticated_sessions_bulk_delete - description: Bulk revoke all sessions for multiple users - parameters: - - in: query - name: user_pks - schema: - type: array - items: - type: integer - description: List of user IDs to revoke all sessions for - required: true - tags: - - core - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/SessionDeleteResponse' - description: '' - '400': - $ref: '#/components/responses/ValidationErrorResponse' - '403': - $ref: '#/components/responses/GenericErrorResponse' /core/authenticated_sessions/{uuid}/: get: operationId: core_authenticated_sessions_retrieve @@ -3116,6 +3088,34 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' + /core/authenticated_sessions/bulk_delete/: + delete: + operationId: core_authenticated_sessions_bulk_delete_destroy + description: Bulk revoke all sessions for multiple users + parameters: + - in: query + name: user_pks + schema: + type: array + items: + type: integer + description: List of user IDs to revoke all sessions for + required: true + tags: + - core + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BulkDeleteSessionResponse' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' /core/brands/: get: operationId: core_brands_list @@ -34725,17 +34725,6 @@ components: - orphaned - unknown type: string - BulkDeleteSession: - type: object - description: Serializer for bulk deleting authenticated sessions by user - properties: - user_ids: - type: array - items: - type: integer - description: List of user IDs to revoke all sessions for - required: - - user_pks Brand: type: object description: Brand Serializer @@ -34878,6 +34867,13 @@ components: additionalProperties: {} required: - domain + BulkDeleteSessionResponse: + type: object + properties: + deleted: + type: integer + required: + - deleted Cache: type: object description: Generic cache stats for an object @@ -53288,15 +53284,6 @@ components: required: - healthy - version - SessionDeleteResponse: - type: object - description: Response for bulk session deletion - properties: - deleted: - type: integer - description: Number of sessions deleted - required: - - deleted SessionEndChallenge: type: object description: Challenge for ending a session diff --git a/web/src/admin/files/FileUploadForm.ts b/web/src/admin/files/FileUploadForm.ts index b62148824b..23cac6934f 100644 --- a/web/src/admin/files/FileUploadForm.ts +++ b/web/src/admin/files/FileUploadForm.ts @@ -45,12 +45,6 @@ function getFileExtension(fileName: string): string { 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 }) @@ -89,17 +83,10 @@ export class FileUploadForm extends Form> { const api = new AdminApi(DEFAULT_CONFIG); 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 = getFileExtension(this.selectedFile.name); - finalName = - ext && !hasBasenameExtension(customName) ? `${customName}${ext}` : customName; - } else { - assertValidFileName(this.selectedFile.name); - } + // If custom name provided, append original file extension; otherwise use original filename + const finalName = customName + ? `${customName}${getFileExtension(this.selectedFile.name)}` + : this.selectedFile.name; assertValidFileName(finalName); diff --git a/web/src/admin/users/UserBulkRevokeSessionsForm.ts b/web/src/admin/users/UserBulkRevokeSessionsForm.ts index 96f1f69e12..b2cfbe4332 100644 --- a/web/src/admin/users/UserBulkRevokeSessionsForm.ts +++ b/web/src/admin/users/UserBulkRevokeSessionsForm.ts @@ -107,7 +107,7 @@ export class UserBulkRevokeSessionsForm extends ModalButton { if (userIds.length > 0) { const response = await new CoreApi( DEFAULT_CONFIG, - ).coreAuthenticatedSessionsBulkDelete({ + ).coreAuthenticatedSessionsBulkDeleteDestroy({ userPks: userIds, }); this.revokedCount = response.deleted || 0;