feat: allow configuring attachment upload limit (#12835)

## Summary
- add a configurable MAXIMUM_FILE_UPLOAD_SIZE installation setting and
surface it through super admin and global config payloads
- apply the configurable limit to attachment validations and shared
upload helpers on dashboard and widget
- introduce a reusable helper with unit tests for parsing the limit and
extend attachment specs for configurability


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912644786b08326bc8dee9401af6d0a)

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
Sojan Jose
2025-11-17 14:03:08 -08:00
committed by GitHub
parent 93374f4327
commit bf806f0c28
16 changed files with 204 additions and 46 deletions

View File

@@ -34,9 +34,6 @@ export const CONVERSATION_PRIORITY_ORDER = {
low: 1,
};
// Size in mega bytes
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
export const ALLOWED_FILE_TYPES =
'image/*,' +
'audio/*,' +

View File

@@ -1,3 +1,5 @@
export const DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE = 40;
export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
@@ -19,3 +21,13 @@ export const checkFileSizeLimit = (file, maximumUploadLimit) => {
const fileSizeInMB = fileSizeInMegaBytes(fileSize);
return fileSizeInMB <= maximumUploadLimit;
};
export const resolveMaximumFileUploadSize = value => {
const parsedValue = Number(value);
if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
return DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE;
}
return parsedValue;
};

View File

@@ -1,7 +1,9 @@
import {
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
formatBytes,
fileSizeInMegaBytes,
checkFileSizeLimit,
resolveMaximumFileUploadSize,
} from '../FileHelper';
describe('#File Helpers', () => {
@@ -19,6 +21,7 @@ describe('#File Helpers', () => {
expect(formatBytes(10000000)).toBe('9.54 MB');
});
});
describe('fileSizeInMegaBytes', () => {
it('should return zero if 0 is passed', () => {
expect(fileSizeInMegaBytes(0)).toBe(0);
@@ -27,6 +30,7 @@ describe('#File Helpers', () => {
expect(fileSizeInMegaBytes(20000000)).toBeCloseTo(19.07, 2);
});
});
describe('checkFileSizeLimit', () => {
it('should return false if file with size 62208194 and file size limit 40 are passed', () => {
expect(checkFileSizeLimit({ file: { size: 62208194 } }, 40)).toBe(false);
@@ -35,4 +39,26 @@ describe('#File Helpers', () => {
expect(checkFileSizeLimit({ file: { size: 199154 } }, 40)).toBe(true);
});
});
describe('resolveMaximumFileUploadSize', () => {
it('should return default when value is undefined', () => {
expect(resolveMaximumFileUploadSize(undefined)).toBe(
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE
);
});
it('should return default when value is not a positive number', () => {
expect(resolveMaximumFileUploadSize('not-a-number')).toBe(
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE
);
expect(resolveMaximumFileUploadSize(-5)).toBe(
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE
);
});
it('should parse numeric strings and numbers', () => {
expect(resolveMaximumFileUploadSize('50')).toBe(50);
expect(resolveMaximumFileUploadSize(75)).toBe(75);
});
});
});

View File

@@ -1,4 +1,5 @@
import { parseBoolean } from '@chatwoot/utils';
import { resolveMaximumFileUploadSize } from 'shared/helpers/FileHelper';
const {
API_CHANNEL_NAME: apiChannelName,
@@ -11,6 +12,7 @@ const {
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
DISPLAY_MANIFEST: displayManifest,
GIT_SHA: gitSha,
MAXIMUM_FILE_UPLOAD_SIZE: maximumFileUploadSize,
HCAPTCHA_SITE_KEY: hCaptchaSiteKey,
INSTALLATION_NAME: installationName,
LOGO_THUMBNAIL: logoThumbnail,
@@ -37,6 +39,7 @@ const state = {
disableUserProfileUpdate: parseBoolean(disableUserProfileUpdate),
displayManifest,
gitSha,
maximumFileUploadSize: resolveMaximumFileUploadSize(maximumFileUploadSize),
hCaptchaSiteKey,
installationName,
logo,