fix: respect allowed_file_types setting for upload validation (#203)

The "Allowed File Types" admin setting was stored in the database but
never actually read during upload validation. Both frontend and backend
used hardcoded MIME type lists, causing video uploads (e.g. MP4) to be
rejected even when explicitly added to the setting.

Changes:
- Add getAllowedMimeTypes() to uploadSettings service that reads the
  general_allowed_file_types DB setting and converts extensions to MIME types
- Backend admin upload route now resolves allowed types from settings
  before multer processes files (via resolveAllowedTypes middleware)
- Backend gallery upload route uses dynamic allowed types from settings
- Expose allowed_file_types in public settings API for gallery clients
- Frontend PhotoUpload and UserPhotoUpload components now derive allowed
  MIME types from settings instead of hardcoded image-only lists
- Add shared fileTypes.ts utility for extension-to-MIME conversion

Closes #203
This commit is contained in:
Paul Nothaft
2026-03-01 14:36:34 +01:00
parent 33483cf32d
commit fe07a148f1
8 changed files with 221 additions and 36 deletions
+32 -24
View File
@@ -9,7 +9,7 @@ const { generateThumbnail, ensureThumbnail, extractCaptureDate } = require('../s
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
const { escapeLikePattern } = require('../utils/sqlSecurity');
const { validateUploadedFiles } = require('../middleware/uploadValidation');
const { getMaxFilesPerUpload } = require('../services/uploadSettings');
const { getMaxFilesPerUpload, getAllowedMimeTypes } = require('../services/uploadSettings');
const { processUploadedPhotos } = require('../services/photoProcessor');
const chunkedUpload = require('../services/chunkedUploadService');
const watermarkGeneratorService = require('../services/watermarkGeneratorService');
@@ -47,47 +47,55 @@ const storage = multer.diskStorage({
}
});
const { validateFileType } = require('../utils/fileSecurityUtils');
const { validateFileType, createFileUploadValidator } = require('../utils/fileSecurityUtils');
// Create a multer instance that uses dynamically resolved allowed MIME types.
// The allowed types are fetched from the database once per request (before multer
// processes files) and attached to req.allowedMimeTypes so that the fileFilter
// callback can read them synchronously.
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 * 1024, // 10GB limit per file to support large videos
files: 2000, // Hard safety ceiling; actual limit enforced dynamically
// Set a reasonable field size limit to prevent memory issues
fieldSize: 10 * 1024 * 1024, // 10MB for non-file fields
// Add part size limits to prevent incomplete uploads
parts: 10000, // Maximum number of parts (fields + files)
headerPairs: 2000 // Maximum number of header key-value pairs
parts: 10000,
headerPairs: 2000
},
fileFilter: (req, file, cb) => {
// Accept images and videos with proper validation
const allowedMimeTypes = [
'image/jpeg', 'image/png', 'image/webp',
'video/mp4', 'video/webm', 'video/quicktime', 'video/x-msvideo'
];
// req.allowedMimeTypes is populated by the middleware that runs before multer
const allowedMimeTypes = req.allowedMimeTypes || ['image/jpeg', 'image/png', 'image/webp'];
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
return cb(null, true);
} else {
cb(new Error('Only JPEG, PNG, WebP images and MP4, WebM, MOV, AVI videos are allowed'));
cb(new Error('Invalid file type. Check allowed file types in system settings.'));
}
},
// Add abort on limit to stop processing when limits are exceeded
abortOnLimit: true
});
const { createFileUploadValidator } = require('../utils/fileSecurityUtils');
// Middleware to resolve allowed MIME types from settings before multer runs
const resolveAllowedTypes = async (req, res, next) => {
try {
req.allowedMimeTypes = await getAllowedMimeTypes();
} catch (error) {
console.error('Failed to resolve allowed MIME types:', error);
req.allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
}
next();
};
// Create content validator middleware
const validateUploadContent = createFileUploadValidator({
allowedTypes: [
'image/jpeg', 'image/png', 'image/webp',
'video/mp4', 'video/webm', 'video/quicktime', 'video/x-msvideo'
],
maxFileSize: 10 * 1024 * 1024 * 1024, // 10GB to support large videos
validateContent: true
});
// Dynamic content validator middleware that reads allowed types from req
const validateUploadContent = async (req, res, next) => {
const allowedTypes = req.allowedMimeTypes || ['image/jpeg', 'image/png', 'image/webp'];
const validator = createFileUploadValidator({
allowedTypes,
maxFileSize: 10 * 1024 * 1024 * 1024, // 10GB to support large videos
validateContent: true
});
return validator(req, res, next);
};
// Request timeout middleware for uploads
const uploadTimeout = (timeout = 300000) => { // 5 minutes default
@@ -111,7 +119,7 @@ const uploadTimeout = (timeout = 300000) => { // 5 minutes default
// Upload photos for an event
// Max file count is configurable via general settings
router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), uploadTimeout(600000), async (req, res, next) => { // 10 minute timeout
router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), uploadTimeout(600000), resolveAllowedTypes, async (req, res, next) => { // 10 minute timeout
let maxFilesPerUpload;
try {
maxFilesPerUpload = await getMaxFilesPerUpload();
+12 -2
View File
@@ -1169,6 +1169,17 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => {
// Import multer and photo processing
const multer = require('multer');
const { getAllowedMimeTypes } = require('../services/uploadSettings');
const { validateFileType } = require('../utils/fileSecurityUtils');
// Resolve allowed MIME types from settings
let allowedMimeTypes;
try {
allowedMimeTypes = await getAllowedMimeTypes();
} catch {
allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
}
const upload = multer({
dest: tempUploadDir,
limits: {
@@ -1176,8 +1187,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, async (req, res) => {
files: 10 // Max 10 files at once
},
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
if (validateFileType(file.originalname, file.mimetype, allowedMimeTypes)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
+2
View File
@@ -78,6 +78,8 @@ router.get('/', async (req, res) => {
event_require_admin_email: settingsObject.event_require_admin_email !== false,
event_require_event_date: settingsObject.event_require_event_date !== false,
event_require_expiration: settingsObject.event_require_expiration !== false,
// Upload settings (safe to expose - needed for client-side validation)
allowed_file_types: settingsObject.general_allowed_file_types || 'jpg,jpeg,png,webp',
// SEO meta tag flags (safe to expose - these are intended for crawlers)
seo_meta_noindex: settingsObject.seo_meta_noindex === true,
seo_meta_nofollow: settingsObject.seo_meta_nofollow === true,