compress: снять лимиты (размер/кол-во/rate-limit) для админа
This commit is contained in:
parent
87f8c55da0
commit
4259c3daf7
@ -120,7 +120,7 @@
|
|||||||
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
Конвертер <span class="text-accent">изображений</span>
|
Конвертер <span class="text-accent">изображений</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">Сжатие, ресайз и конвертация · до 50 файлов · до 20 МБ</p>
|
<p id="limitsHint" class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">Сжатие, ресайз и конвертация · до 50 файлов · до 20 МБ</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -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 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) {
|
function handleFiles(files) {
|
||||||
const arr = Array.from(files);
|
const arr = Array.from(files);
|
||||||
|
|||||||
@ -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 maxFileSize = (parseInt(process.env.MAX_FILE_SIZE_MB) || 20) * 1024 * 1024;
|
||||||
const maxFiles = parseInt(process.env.MAX_FILES) || 50;
|
const maxFiles = parseInt(process.env.MAX_FILES) || 50;
|
||||||
|
|
||||||
const upload = multer({
|
const imageFileFilter = (req, file, cb) => {
|
||||||
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'];
|
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);
|
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('Неподдерживаемый формат изображения'));
|
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({
|
const limiter = rateLimit({
|
||||||
keyGenerator: (req) => (req.session && req.session.user && req.session.user.id) ? "user_" + req.session.user.id : req.ip,
|
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,
|
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60000,
|
||||||
max: parseInt(process.env.RATE_LIMIT_MAX) || 30,
|
max: parseInt(process.env.RATE_LIMIT_MAX) || 30,
|
||||||
|
skip: isAdmin,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
message: { error: 'Слишком много запросов. Попробуйте через минуту.' },
|
message: { error: 'Слишком много запросов. Попробуйте через минуту.' },
|
||||||
@ -104,7 +118,7 @@ router.get('/', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Compress endpoint
|
// 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 resize = parseInt(req.body.resize || '0', 10);
|
||||||
const format = (req.body.format || 'original').toLowerCase();
|
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)));
|
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
|
// 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 resize = parseInt(req.body.resize || '0', 10);
|
||||||
const format = (req.body.format || 'original').toLowerCase();
|
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)));
|
const quality = Math.min(100, Math.max(1, parseInt(req.body.quality || process.env.COMPRESS_QUALITY || '80', 10)));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user