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
This commit is contained in:
Dominic R
2026-01-22 19:39:10 -05:00
committed by GitHub
parent 7550b85495
commit c67447d4db
3 changed files with 40 additions and 66 deletions
+35 -48
View File
@@ -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
+4 -17
View File
@@ -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<Record<string, unknown>> {
@property({ type: String, useDefault: true })
@@ -89,17 +83,10 @@ export class FileUploadForm extends Form<Record<string, unknown>> {
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);
@@ -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;