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:
@@ -9,7 +9,13 @@ class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_global_config
|
def set_global_config
|
||||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME')
|
@global_config = GlobalConfig.get(
|
||||||
|
'LOGO_THUMBNAIL',
|
||||||
|
'BRAND_NAME',
|
||||||
|
'WIDGET_BRAND_URL',
|
||||||
|
'MAXIMUM_FILE_UPLOAD_SIZE',
|
||||||
|
'INSTALLATION_NAME'
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_contact
|
def set_contact
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
class DashboardController < ActionController::Base
|
class DashboardController < ActionController::Base
|
||||||
include SwitchLocale
|
include SwitchLocale
|
||||||
|
|
||||||
|
GLOBAL_CONFIG_KEYS = %w[
|
||||||
|
LOGO
|
||||||
|
LOGO_DARK
|
||||||
|
LOGO_THUMBNAIL
|
||||||
|
INSTALLATION_NAME
|
||||||
|
WIDGET_BRAND_URL
|
||||||
|
TERMS_URL
|
||||||
|
BRAND_URL
|
||||||
|
BRAND_NAME
|
||||||
|
PRIVACY_URL
|
||||||
|
DISPLAY_MANIFEST
|
||||||
|
CREATE_NEW_ACCOUNT_FROM_DASHBOARD
|
||||||
|
CHATWOOT_INBOX_TOKEN
|
||||||
|
API_CHANNEL_NAME
|
||||||
|
API_CHANNEL_THUMBNAIL
|
||||||
|
ANALYTICS_TOKEN
|
||||||
|
DIRECT_UPLOADS_ENABLED
|
||||||
|
MAXIMUM_FILE_UPLOAD_SIZE
|
||||||
|
HCAPTCHA_SITE_KEY
|
||||||
|
LOGOUT_REDIRECT_LINK
|
||||||
|
DISABLE_USER_PROFILE_UPDATE
|
||||||
|
DEPLOYMENT_ENV
|
||||||
|
INSTALLATION_PRICING_PLAN
|
||||||
|
].freeze
|
||||||
|
|
||||||
before_action :set_application_pack
|
before_action :set_application_pack
|
||||||
before_action :set_global_config
|
before_action :set_global_config
|
||||||
before_action :set_dashboard_scripts
|
before_action :set_dashboard_scripts
|
||||||
@@ -19,25 +44,7 @@ class DashboardController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_global_config
|
def set_global_config
|
||||||
@global_config = GlobalConfig.get(
|
@global_config = GlobalConfig.get(*GLOBAL_CONFIG_KEYS).merge(app_config)
|
||||||
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
|
|
||||||
'INSTALLATION_NAME',
|
|
||||||
'WIDGET_BRAND_URL', 'TERMS_URL',
|
|
||||||
'BRAND_URL', 'BRAND_NAME',
|
|
||||||
'PRIVACY_URL',
|
|
||||||
'DISPLAY_MANIFEST',
|
|
||||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
|
||||||
'CHATWOOT_INBOX_TOKEN',
|
|
||||||
'API_CHANNEL_NAME',
|
|
||||||
'API_CHANNEL_THUMBNAIL',
|
|
||||||
'ANALYTICS_TOKEN',
|
|
||||||
'DIRECT_UPLOADS_ENABLED',
|
|
||||||
'HCAPTCHA_SITE_KEY',
|
|
||||||
'LOGOUT_REDIRECT_LINK',
|
|
||||||
'DISABLE_USER_PROFILE_UPDATE',
|
|
||||||
'DEPLOYMENT_ENV',
|
|
||||||
'INSTALLATION_PRICING_PLAN'
|
|
||||||
).merge(app_config)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_dashboard_scripts
|
def set_dashboard_scripts
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
|||||||
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI]
|
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI]
|
||||||
}
|
}
|
||||||
|
|
||||||
@allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS])
|
@allowed_configs = mapping.fetch(
|
||||||
|
@config,
|
||||||
|
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS MAXIMUM_FILE_UPLOAD_SIZE]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ class WidgetsController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_global_config
|
def set_global_config
|
||||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED', 'INSTALLATION_NAME')
|
@global_config = GlobalConfig.get(
|
||||||
|
'LOGO_THUMBNAIL',
|
||||||
|
'BRAND_NAME',
|
||||||
|
'WIDGET_BRAND_URL',
|
||||||
|
'DIRECT_UPLOADS_ENABLED',
|
||||||
|
'MAXIMUM_FILE_UPLOAD_SIZE',
|
||||||
|
'INSTALLATION_NAME'
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_web_widget
|
def set_web_widget
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ vi.mock('dashboard/composables', () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock('vue-i18n');
|
vi.mock('vue-i18n');
|
||||||
vi.mock('activestorage');
|
vi.mock('activestorage');
|
||||||
vi.mock('shared/helpers/FileHelper');
|
vi.mock('shared/helpers/FileHelper', () => ({
|
||||||
|
checkFileSizeLimit: vi.fn(),
|
||||||
|
resolveMaximumFileUploadSize: vi.fn(value => Number(value) || 40),
|
||||||
|
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE: 40,
|
||||||
|
}));
|
||||||
vi.mock('@chatwoot/utils');
|
vi.mock('@chatwoot/utils');
|
||||||
|
|
||||||
describe('useFileUpload', () => {
|
describe('useFileUpload', () => {
|
||||||
@@ -36,7 +40,9 @@ describe('useFileUpload', () => {
|
|||||||
getCurrentAccountId: { value: '123' },
|
getCurrentAccountId: { value: '123' },
|
||||||
getCurrentUser: { value: { access_token: 'test-token' } },
|
getCurrentUser: { value: { access_token: 'test-token' } },
|
||||||
getSelectedChat: { value: { id: '456' } },
|
getSelectedChat: { value: { id: '456' } },
|
||||||
'globalConfig/get': { value: { directUploadsEnabled: true } },
|
'globalConfig/get': {
|
||||||
|
value: { directUploadsEnabled: true, maximumFileUploadSize: 40 },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return getterMap[getter];
|
return getterMap[getter];
|
||||||
});
|
});
|
||||||
@@ -86,7 +92,9 @@ describe('useFileUpload', () => {
|
|||||||
getCurrentAccountId: { value: '123' },
|
getCurrentAccountId: { value: '123' },
|
||||||
getCurrentUser: { value: { access_token: 'test-token' } },
|
getCurrentUser: { value: { access_token: 'test-token' } },
|
||||||
getSelectedChat: { value: { id: '456' } },
|
getSelectedChat: { value: { id: '456' } },
|
||||||
'globalConfig/get': { value: { directUploadsEnabled: false } },
|
'globalConfig/get': {
|
||||||
|
value: { directUploadsEnabled: false, maximumFileUploadSize: 40 },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return getterMap[getter];
|
return getterMap[getter];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { DirectUpload } from 'activestorage';
|
import { DirectUpload } from 'activestorage';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
|
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
|
||||||
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
import {
|
||||||
|
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
|
resolveMaximumFileUploadSize,
|
||||||
|
} from 'shared/helpers/FileHelper';
|
||||||
|
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable for handling file uploads in conversations
|
* Composable for handling file uploads in conversations
|
||||||
@@ -21,18 +25,34 @@ export const useFileUpload = ({ inbox, attachFile, isPrivateNote = false }) => {
|
|||||||
const currentChat = useMapGetter('getSelectedChat');
|
const currentChat = useMapGetter('getSelectedChat');
|
||||||
const globalConfig = useMapGetter('globalConfig/get');
|
const globalConfig = useMapGetter('globalConfig/get');
|
||||||
|
|
||||||
|
const installationLimit = resolveMaximumFileUploadSize(
|
||||||
|
globalConfig.value?.maximumFileUploadSize
|
||||||
|
);
|
||||||
|
|
||||||
// helper: compute max upload size for a given file's mime
|
// helper: compute max upload size for a given file's mime
|
||||||
const maxSizeFor = mime => {
|
const maxSizeFor = mime => {
|
||||||
// Use default file size limit for private notes
|
// Use default/installation limit for private notes
|
||||||
if (isPrivateNote) {
|
if (isPrivateNote) {
|
||||||
return MAXIMUM_FILE_UPLOAD_SIZE;
|
return installationLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMaxUploadSizeByChannel({
|
const channelType = inbox?.channel_type;
|
||||||
channelType: inbox?.channel_type,
|
|
||||||
|
if (!channelType || channelType === INBOX_TYPES.WEB) {
|
||||||
|
return installationLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelLimit = getMaxUploadSizeByChannel({
|
||||||
|
channelType,
|
||||||
medium: inbox?.medium, // e.g. 'sms' | 'whatsapp' | etc.
|
medium: inbox?.medium, // e.g. 'sms' | 'whatsapp' | etc.
|
||||||
mime, // e.g. 'image/png'
|
mime, // e.g. 'image/png'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (channelLimit === DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE) {
|
||||||
|
return installationLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(channelLimit, installationLimit);
|
||||||
};
|
};
|
||||||
|
|
||||||
const alertOverLimit = maxSizeMB =>
|
const alertOverLimit = maxSizeMB =>
|
||||||
|
|||||||
@@ -3,27 +3,48 @@ import { useAlert } from 'dashboard/composables';
|
|||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
|
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
|
||||||
import { DirectUpload } from 'activestorage';
|
import { DirectUpload } from 'activestorage';
|
||||||
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
import {
|
||||||
|
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
|
resolveMaximumFileUploadSize,
|
||||||
|
} from 'shared/helpers/FileHelper';
|
||||||
|
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
|
installationLimit() {
|
||||||
|
return resolveMaximumFileUploadSize(
|
||||||
|
this.globalConfig.maximumFileUploadSize
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
maxSizeFor(mime) {
|
maxSizeFor(mime) {
|
||||||
// Use default file size limit for private notes
|
// Use default/installation limit for private notes
|
||||||
if (this.isOnPrivateNote) {
|
if (this.isOnPrivateNote) {
|
||||||
return MAXIMUM_FILE_UPLOAD_SIZE;
|
return this.installationLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMaxUploadSizeByChannel({
|
const channelType = this.inbox?.channel_type;
|
||||||
channelType: this.inbox?.channel_type,
|
|
||||||
|
if (!channelType || channelType === INBOX_TYPES.WEB) {
|
||||||
|
return this.installationLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelLimit = getMaxUploadSizeByChannel({
|
||||||
|
channelType,
|
||||||
medium: this.inbox?.medium, // e.g. 'sms' | 'whatsapp'
|
medium: this.inbox?.medium, // e.g. 'sms' | 'whatsapp'
|
||||||
mime, // e.g. 'image/png'
|
mime, // e.g. 'image/png'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (channelLimit === DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE) {
|
||||||
|
return this.installationLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(channelLimit, this.installationLimit);
|
||||||
},
|
},
|
||||||
alertOverLimit(maxSizeMB) {
|
alertOverLimit(maxSizeMB) {
|
||||||
useAlert(
|
useAlert(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { reactive } from 'vue';
|
|||||||
|
|
||||||
vi.mock('shared/helpers/FileHelper', () => ({
|
vi.mock('shared/helpers/FileHelper', () => ({
|
||||||
checkFileSizeLimit: vi.fn(),
|
checkFileSizeLimit: vi.fn(),
|
||||||
|
resolveMaximumFileUploadSize: vi.fn(value => Number(value) || 40),
|
||||||
|
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE: 40,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('activestorage', () => ({
|
vi.mock('activestorage', () => ({
|
||||||
@@ -27,6 +29,7 @@ describe('FileUploadMixin', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGlobalConfig = reactive({
|
mockGlobalConfig = reactive({
|
||||||
directUploadsEnabled: true,
|
directUploadsEnabled: true,
|
||||||
|
maximumFileUploadSize: 40,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockCurrentChat = reactive({
|
mockCurrentChat = reactive({
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ export const CONVERSATION_PRIORITY_ORDER = {
|
|||||||
low: 1,
|
low: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Size in mega bytes
|
|
||||||
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
|
|
||||||
|
|
||||||
export const ALLOWED_FILE_TYPES =
|
export const ALLOWED_FILE_TYPES =
|
||||||
'image/*,' +
|
'image/*,' +
|
||||||
'audio/*,' +
|
'audio/*,' +
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export const DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE = 40;
|
||||||
|
|
||||||
export const formatBytes = (bytes, decimals = 2) => {
|
export const formatBytes = (bytes, decimals = 2) => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
@@ -19,3 +21,13 @@ export const checkFileSizeLimit = (file, maximumUploadLimit) => {
|
|||||||
const fileSizeInMB = fileSizeInMegaBytes(fileSize);
|
const fileSizeInMB = fileSizeInMegaBytes(fileSize);
|
||||||
return fileSizeInMB <= maximumUploadLimit;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
DEFAULT_MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
fileSizeInMegaBytes,
|
fileSizeInMegaBytes,
|
||||||
checkFileSizeLimit,
|
checkFileSizeLimit,
|
||||||
|
resolveMaximumFileUploadSize,
|
||||||
} from '../FileHelper';
|
} from '../FileHelper';
|
||||||
|
|
||||||
describe('#File Helpers', () => {
|
describe('#File Helpers', () => {
|
||||||
@@ -19,6 +21,7 @@ describe('#File Helpers', () => {
|
|||||||
expect(formatBytes(10000000)).toBe('9.54 MB');
|
expect(formatBytes(10000000)).toBe('9.54 MB');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fileSizeInMegaBytes', () => {
|
describe('fileSizeInMegaBytes', () => {
|
||||||
it('should return zero if 0 is passed', () => {
|
it('should return zero if 0 is passed', () => {
|
||||||
expect(fileSizeInMegaBytes(0)).toBe(0);
|
expect(fileSizeInMegaBytes(0)).toBe(0);
|
||||||
@@ -27,6 +30,7 @@ describe('#File Helpers', () => {
|
|||||||
expect(fileSizeInMegaBytes(20000000)).toBeCloseTo(19.07, 2);
|
expect(fileSizeInMegaBytes(20000000)).toBeCloseTo(19.07, 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkFileSizeLimit', () => {
|
describe('checkFileSizeLimit', () => {
|
||||||
it('should return false if file with size 62208194 and file size limit 40 are passed', () => {
|
it('should return false if file with size 62208194 and file size limit 40 are passed', () => {
|
||||||
expect(checkFileSizeLimit({ file: { size: 62208194 } }, 40)).toBe(false);
|
expect(checkFileSizeLimit({ file: { size: 62208194 } }, 40)).toBe(false);
|
||||||
@@ -35,4 +39,26 @@ describe('#File Helpers', () => {
|
|||||||
expect(checkFileSizeLimit({ file: { size: 199154 } }, 40)).toBe(true);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { parseBoolean } from '@chatwoot/utils';
|
import { parseBoolean } from '@chatwoot/utils';
|
||||||
|
import { resolveMaximumFileUploadSize } from 'shared/helpers/FileHelper';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
API_CHANNEL_NAME: apiChannelName,
|
API_CHANNEL_NAME: apiChannelName,
|
||||||
@@ -11,6 +12,7 @@ const {
|
|||||||
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
|
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
|
||||||
DISPLAY_MANIFEST: displayManifest,
|
DISPLAY_MANIFEST: displayManifest,
|
||||||
GIT_SHA: gitSha,
|
GIT_SHA: gitSha,
|
||||||
|
MAXIMUM_FILE_UPLOAD_SIZE: maximumFileUploadSize,
|
||||||
HCAPTCHA_SITE_KEY: hCaptchaSiteKey,
|
HCAPTCHA_SITE_KEY: hCaptchaSiteKey,
|
||||||
INSTALLATION_NAME: installationName,
|
INSTALLATION_NAME: installationName,
|
||||||
LOGO_THUMBNAIL: logoThumbnail,
|
LOGO_THUMBNAIL: logoThumbnail,
|
||||||
@@ -37,6 +39,7 @@ const state = {
|
|||||||
disableUserProfileUpdate: parseBoolean(disableUserProfileUpdate),
|
disableUserProfileUpdate: parseBoolean(disableUserProfileUpdate),
|
||||||
displayManifest,
|
displayManifest,
|
||||||
gitSha,
|
gitSha,
|
||||||
|
maximumFileUploadSize: resolveMaximumFileUploadSize(maximumFileUploadSize),
|
||||||
hCaptchaSiteKey,
|
hCaptchaSiteKey,
|
||||||
installationName,
|
installationName,
|
||||||
logo,
|
logo,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
|
||||||
import {
|
import {
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE,
|
checkFileSizeLimit,
|
||||||
ALLOWED_FILE_TYPES,
|
resolveMaximumFileUploadSize,
|
||||||
} from 'shared/constants/messages';
|
} from 'shared/helpers/FileHelper';
|
||||||
|
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
import { DirectUpload } from 'activestorage';
|
import { DirectUpload } from 'activestorage';
|
||||||
@@ -29,7 +29,9 @@ export default {
|
|||||||
shouldShowFilePicker: 'appConfig/getShouldShowFilePicker',
|
shouldShowFilePicker: 'appConfig/getShouldShowFilePicker',
|
||||||
}),
|
}),
|
||||||
fileUploadSizeLimit() {
|
fileUploadSizeLimit() {
|
||||||
return MAXIMUM_FILE_UPLOAD_SIZE;
|
return resolveMaximumFileUploadSize(
|
||||||
|
this.globalConfig.maximumFileUploadSize
|
||||||
|
);
|
||||||
},
|
},
|
||||||
allowedFileTypes() {
|
allowedFileTypes() {
|
||||||
return ALLOWED_FILE_TYPES;
|
return ALLOWED_FILE_TYPES;
|
||||||
@@ -73,7 +75,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.isUploading = true;
|
this.isUploading = true;
|
||||||
try {
|
try {
|
||||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
if (checkFileSizeLimit(file, this.fileUploadSizeLimit)) {
|
||||||
const { websiteToken } = window.chatwootWebChannel;
|
const { websiteToken } = window.chatwootWebChannel;
|
||||||
const upload = new DirectUpload(
|
const upload = new DirectUpload(
|
||||||
file.file,
|
file.file,
|
||||||
@@ -115,7 +117,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.isUploading = true;
|
this.isUploading = true;
|
||||||
try {
|
try {
|
||||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
if (checkFileSizeLimit(file, this.fileUploadSizeLimit)) {
|
||||||
await this.onAttach({
|
await this.onAttach({
|
||||||
file: file.file,
|
file: file.file,
|
||||||
...this.getLocalFileAttributes(file),
|
...this.getLocalFileAttributes(file),
|
||||||
|
|||||||
@@ -166,7 +166,10 @@ class Attachment < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_file_size(byte_size)
|
def validate_file_size(byte_size)
|
||||||
errors.add(:file, 'size is too big') if byte_size > 40.megabytes
|
limit_mb = GlobalConfigService.load('MAXIMUM_FILE_UPLOAD_SIZE', 40).to_i
|
||||||
|
limit_mb = 40 if limit_mb <= 0
|
||||||
|
|
||||||
|
errors.add(:file, 'size is too big') if byte_size > limit_mb.megabytes
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_file?(file_content_type)
|
def media_file?(file_content_type)
|
||||||
|
|||||||
@@ -84,6 +84,11 @@
|
|||||||
value: false
|
value: false
|
||||||
description: 'Enable direct uploads to cloud storage'
|
description: 'Enable direct uploads to cloud storage'
|
||||||
locked: false
|
locked: false
|
||||||
|
- name: MAXIMUM_FILE_UPLOAD_SIZE
|
||||||
|
value: 40
|
||||||
|
display_title: 'Attachment size limit (MB)'
|
||||||
|
description: 'Maximum attachment size in MB allowed for uploads'
|
||||||
|
locked: false
|
||||||
# ------- End of Account Related Config ------- #
|
# ------- End of Account Related Config ------- #
|
||||||
|
|
||||||
# ------- Email Related Config ------- #
|
# ------- Email Related Config ------- #
|
||||||
|
|||||||
@@ -154,4 +154,39 @@ RSpec.describe Attachment do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'file size validation' do
|
||||||
|
let(:attachment) { message.attachments.new(account_id: message.account_id, file_type: :image) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(GlobalConfigService).to receive(:load).and_call_original
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'respects configured limit' do
|
||||||
|
allow(GlobalConfigService).to receive(:load)
|
||||||
|
.with('MAXIMUM_FILE_UPLOAD_SIZE', 40)
|
||||||
|
.and_return('5')
|
||||||
|
|
||||||
|
attachment.errors.clear
|
||||||
|
attachment.send(:validate_file_size, 4.megabytes)
|
||||||
|
|
||||||
|
expect(attachment.errors[:file]).to be_empty
|
||||||
|
|
||||||
|
attachment.errors.clear
|
||||||
|
attachment.send(:validate_file_size, 6.megabytes)
|
||||||
|
|
||||||
|
expect(attachment.errors[:file]).to include('size is too big')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'falls back to default when configured limit is invalid' do
|
||||||
|
allow(GlobalConfigService).to receive(:load)
|
||||||
|
.with('MAXIMUM_FILE_UPLOAD_SIZE', 40)
|
||||||
|
.and_return('-10')
|
||||||
|
|
||||||
|
attachment.errors.clear
|
||||||
|
attachment.send(:validate_file_size, 41.megabytes)
|
||||||
|
|
||||||
|
expect(attachment.errors[:file]).to include('size is too big')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user