Files
picpeak/backend/src/config/multerConfig.js
T
Paul Nothaft 3424bd22ee refactor: Phase 1 code consolidation and service layer setup
Phase 1.1: Shared parsers utility
- Create backend/src/utils/parsers.js with parseBooleanInput, parseStringInput, etc.
- Create frontend/src/utils/parsers.ts with TypeScript equivalents
- Update routes to import from shared parsers

Phase 1.2: Auth routes consolidation
- Merge auth.js, auth-enhanced.js, auth-enhanced-v2.js into single auth.js
- Add password change and password strength endpoints
- Consolidate middleware (auth.js with token revocation support)
- Update all imports across 14+ route files

Phase 1.3: CreateEvent page consolidation
- Remove duplicate CreateEventPage.tsx (basic version)
- Rename CreateEventPageEnhanced.tsx to CreateEventPage.tsx
- Update exports and imports

Phase 1.4: CMS page consolidation
- Remove duplicate CMSPage.tsx (basic version)
- Rename CMSPageEnhanced.tsx to CMSPage.tsx
- Update exports and imports

Phase 1.5: Multer config factory
- Create backend/src/config/multerConfig.js
- Centralized upload configuration with presets for photos, logos, favicons
- Reusable helpers: createDiskStorage, createFileFilter, uploadTimeoutMiddleware

Phase 2.1: Event service layer
- Create backend/src/services/eventService.js
- Move event business logic out of routes
- Functions: createEvent, getAllEvents, updateEvent, deleteEvent, extendExpiration
2026-01-02 10:12:24 +01:00

256 lines
7.2 KiB
JavaScript

/**
* Centralized Multer Configuration Factory
* Provides pre-configured multer instances for different upload scenarios
*
* @module config/multerConfig
*/
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { validateFileType } = require('../utils/fileSecurityUtils');
/**
* Get the storage path from environment or default
* @returns {string}
*/
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
/**
* Default allowed MIME types for different upload types
*/
const ALLOWED_TYPES = {
photos: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
videos: ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm'],
media: ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm'],
logos: ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'],
favicons: ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon'],
documents: ['application/pdf', 'text/plain']
};
/**
* Default file size limits (in bytes)
*/
const SIZE_LIMITS = {
small: 1 * 1024 * 1024, // 1MB
medium: 5 * 1024 * 1024, // 5MB
large: 50 * 1024 * 1024, // 50MB
xlarge: 500 * 1024 * 1024, // 500MB
huge: 10 * 1024 * 1024 * 1024 // 10GB (for large videos)
};
/**
* Create a disk storage configuration
*
* @param {Object} options - Storage options
* @param {string} options.subdir - Subdirectory within storage path
* @param {Function} [options.filename] - Custom filename generator
* @param {boolean} [options.useTemp] - Use temp directory instead
* @returns {multer.StorageEngine}
*/
const createDiskStorage = (options = {}) => {
const { subdir, filename, useTemp = false } = options;
return multer.diskStorage({
destination: async (req, file, cb) => {
try {
let uploadDir;
if (useTemp) {
uploadDir = path.join(getStoragePath(), 'temp', `upload_${Date.now()}_${Math.random().toString(36).substring(7)}`);
} else {
uploadDir = path.join(getStoragePath(), subdir || 'uploads');
}
// Create directory synchronously to prevent race conditions
fs.mkdirSync(uploadDir, { recursive: true });
cb(null, uploadDir);
} catch (error) {
cb(error);
}
},
filename: filename || ((req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
const ext = path.extname(file.originalname);
const baseName = path.basename(file.originalname, ext).replace(/[^a-zA-Z0-9-_]/g, '_');
cb(null, `${baseName}-${uniqueSuffix}${ext}`);
})
});
};
/**
* Create a file filter function
*
* @param {string[]} allowedTypes - Array of allowed MIME types
* @param {Object} [options] - Filter options
* @param {boolean} [options.validateMagicNumbers] - Whether to validate file magic numbers
* @param {string[]} [options.skipMagicValidation] - MIME types to skip magic number validation for
* @returns {Function} Multer file filter function
*/
const createFileFilter = (allowedTypes, options = {}) => {
const { validateMagicNumbers = true, skipMagicValidation = [] } = options;
return (req, file, cb) => {
// Basic MIME type check
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error(`File type ${file.mimetype} not allowed. Allowed types: ${allowedTypes.join(', ')}`));
}
// Validate file type with magic numbers (if enabled and not skipped)
if (validateMagicNumbers && !skipMagicValidation.includes(file.mimetype)) {
if (validateFileType && !validateFileType(file.originalname, file.mimetype, allowedTypes)) {
return cb(new Error('File content does not match file type'));
}
}
cb(null, true);
};
};
/**
* Create a multer instance for photo uploads
*
* @param {Object} [options] - Override options
* @returns {multer.Multer}
*/
const createPhotoUploader = (options = {}) => {
const defaults = {
storage: createDiskStorage({ useTemp: true }),
limits: {
fileSize: options.maxSize || SIZE_LIMITS.huge,
files: options.maxFiles || 2000,
fieldSize: 10 * 1024 * 1024,
parts: 10000,
headerPairs: 2000
},
fileFilter: createFileFilter(ALLOWED_TYPES.media, {
validateMagicNumbers: true
})
};
return multer({ ...defaults, ...options });
};
/**
* Create a multer instance for logo uploads
*
* @param {Object} [options] - Override options
* @returns {multer.Multer}
*/
const createLogoUploader = (options = {}) => {
const defaults = {
storage: createDiskStorage({
subdir: 'uploads/logos',
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `logo-${Date.now()}${ext}`);
}
}),
limits: {
fileSize: options.maxSize || SIZE_LIMITS.medium
},
fileFilter: createFileFilter(ALLOWED_TYPES.logos, {
skipMagicValidation: ['image/svg+xml']
})
};
return multer({ ...defaults, ...options });
};
/**
* Create a multer instance for favicon uploads
*
* @param {Object} [options] - Override options
* @returns {multer.Multer}
*/
const createFaviconUploader = (options = {}) => {
const defaults = {
storage: createDiskStorage({
subdir: 'uploads/favicons',
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `favicon-${Date.now()}${ext}`);
}
}),
limits: {
fileSize: options.maxSize || SIZE_LIMITS.small
},
fileFilter: createFileFilter(ALLOWED_TYPES.favicons, {
skipMagicValidation: ['image/x-icon', 'image/vnd.microsoft.icon']
})
};
return multer({ ...defaults, ...options });
};
/**
* Create a multer instance for gallery user uploads
*
* @param {string} destDir - Destination directory
* @param {Object} [options] - Override options
* @returns {multer.Multer}
*/
const createGalleryUploader = (destDir, options = {}) => {
const defaults = {
dest: destDir,
limits: {
fileSize: options.maxSize || SIZE_LIMITS.large,
files: options.maxFiles || 10
},
fileFilter: createFileFilter(ALLOWED_TYPES.photos)
};
return multer({ ...defaults, ...options });
};
/**
* Create a custom multer instance
*
* @param {Object} config - Full multer configuration
* @returns {multer.Multer}
*/
const createCustomUploader = (config) => {
return multer(config);
};
/**
* Upload timeout middleware
*
* @param {number} [timeout=300000] - Timeout in milliseconds (default 5 minutes)
* @returns {Function} Express middleware
*/
const uploadTimeoutMiddleware = (timeout = 300000) => {
return (req, res, next) => {
req.setTimeout(timeout, () => {
console.error('Upload request timed out');
if (!res.headersSent) {
res.status(408).json({ error: 'Upload request timed out' });
}
});
res.setTimeout(timeout, () => {
console.error('Upload response timed out');
});
next();
};
};
module.exports = {
// Pre-configured uploaders
createPhotoUploader,
createLogoUploader,
createFaviconUploader,
createGalleryUploader,
createCustomUploader,
// Building blocks for custom configurations
createDiskStorage,
createFileFilter,
// Middleware
uploadTimeoutMiddleware,
// Constants
ALLOWED_TYPES,
SIZE_LIMITS
};