diff --git a/public/compress.html b/public/compress.html index eda41e1..7ec1616 100644 --- a/public/compress.html +++ b/public/compress.html @@ -120,7 +120,7 @@

Конвертер изображений

-

Сжатие, ресайз и конвертация · до 50 файлов · до 20 МБ

+

Сжатие, ресайз и конвертация · до 50 файлов · до 20 МБ

@@ -297,7 +297,17 @@ document.getElementById('resize').addEventListener('change', function() { }); const VALID_TYPES = ['image/jpeg','image/png','image/webp','image/avif','image/tiff','image/gif','image/bmp','image/svg+xml']; -const MAX_SIZE = 20 * 1024 * 1024; +let MAX_SIZE = 20 * 1024 * 1024; +let isAdmin = false; +// Админу — без лимитов по размеру/количеству +(function(){ fetch('/auth/me').then(function(r){return r.ok?r.json():null}).then(function(u){ + if (u && u.role === 'admin') { + isAdmin = true; + MAX_SIZE = Infinity; + var d = document.getElementById('limitsHint'); + if (d) d.textContent = 'Сжатие, ресайз и конвертация · без лимитов'; + } +}).catch(function(){}); })(); function handleFiles(files) { const arr = Array.from(files); diff --git a/routes/compress.js b/routes/compress.js index dc0f57b..a0cad9a 100644 --- a/routes/compress.js +++ b/routes/compress.js @@ -35,21 +35,35 @@ setInterval(() => cleanupDir(RESULTS_DIR, 30 * 60 * 1000), 5 * 60 * 1000); const maxFileSize = (parseInt(process.env.MAX_FILE_SIZE_MB) || 20) * 1024 * 1024; const maxFiles = parseInt(process.env.MAX_FILES) || 50; -const upload = multer({ - dest: UPLOADS_DIR, - limits: { fileSize: maxFileSize }, - fileFilter: (req, file, cb) => { - const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/tiff', 'image/gif', 'image/bmp', 'image/svg+xml']; - if (validTypes.includes(file.mimetype) || file.originalname.match(/\.(jpe?g|png|webp|avif|tiff?|gif|bmp|svg|heic|heif)$/i)) cb(null, true); - else cb(new Error('Неподдерживаемый формат изображения')); - }, -}); +const imageFileFilter = (req, file, cb) => { + const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/tiff', 'image/gif', 'image/bmp', 'image/svg+xml']; + if (validTypes.includes(file.mimetype) || file.originalname.match(/\.(jpe?g|png|webp|avif|tiff?|gif|bmp|svg|heic|heif)$/i)) cb(null, true); + else cb(new Error('Неподдерживаемый формат изображения')); +}; -// Rate limiter +// Админ — без лимитов (размер, кол-во файлов, rate-limit) +function isAdmin(req) { + return !!(req.session && req.session.user && req.session.user.role === 'admin'); +} + +// User — лимит размера из .env; Admin — без лимита размера +const uploadUser = multer({ dest: UPLOADS_DIR, limits: { fileSize: maxFileSize }, fileFilter: imageFileFilter }); +const uploadAdmin = multer({ dest: UPLOADS_DIR, fileFilter: imageFileFilter }); + +// Динамический выбор multer: админу — без лимита количества и размера +function uploadArray(req, res, next) { + return (isAdmin(req) ? uploadAdmin.array('images') : uploadUser.array('images', maxFiles))(req, res, next); +} +function uploadSingle(req, res, next) { + return (isAdmin(req) ? uploadAdmin : uploadUser).single('image')(req, res, next); +} + +// Rate limiter (админ пропускается — безлимитная загрузка по одному файлу на запрос) const limiter = rateLimit({ keyGenerator: (req) => (req.session && req.session.user && req.session.user.id) ? "user_" + req.session.user.id : req.ip, windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60000, max: parseInt(process.env.RATE_LIMIT_MAX) || 30, + skip: isAdmin, standardHeaders: true, legacyHeaders: false, message: { error: 'Слишком много запросов. Попробуйте через минуту.' }, @@ -104,7 +118,7 @@ router.get('/', (req, res) => { }); // Compress endpoint -router.post('/', limiter, upload.array('images', maxFiles), async (req, res) => { +router.post('/', limiter, uploadArray, async (req, res) => { const resize = parseInt(req.body.resize || '0', 10); const format = (req.body.format || 'original').toLowerCase(); const quality = Math.min(100, Math.max(1, parseInt(req.body.quality || process.env.COMPRESS_QUALITY || '80', 10))); @@ -196,7 +210,7 @@ router.post('/', limiter, upload.array('images', maxFiles), async (req, res) => // Single file compress endpoint -router.post('/single', limiter, upload.single('image'), async (req, res) => { +router.post('/single', limiter, uploadSingle, async (req, res) => { const resize = parseInt(req.body.resize || '0', 10); const format = (req.body.format || 'original').toLowerCase(); const quality = Math.min(100, Math.max(1, parseInt(req.body.quality || process.env.COMPRESS_QUALITY || '80', 10)));