Phase 3 + 6

This commit is contained in:
2026-05-23 19:38:12 -04:00
parent 1ab14ba45f
commit 82e568d455
60 changed files with 1983 additions and 2720 deletions
+37 -19
View File
@@ -1,14 +1,23 @@
const express = require('express');
import express from 'express';
import { Client } from 'ssh2';
import mysql from 'mysql2/promise';
import multer from 'multer';
import path from 'node:path';
import fs from 'node:fs';
import sharp from 'sharp';
import axios from 'axios';
import net from 'node:net';
import { requirePermission } from '../../shared/auth/middleware.js';
const router = express.Router();
const { Client } = require('ssh2');
const mysql = require('mysql2/promise');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const fsp = fs.promises;
const sharp = require('sharp');
const axios = require('axios');
const net = require('net');
// Phase 6.2: imports, uploads, generate-upc and deletions all require product_import.
// Reads (list-uploads, status checks) remain authenticated-only.
router.use((req, res, next) => {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') return next();
return requirePermission('product_import')(req, res, next);
});
// Create uploads directory if it doesn't exist
const uploadsDir = path.join('/var/www/inventory/uploads/products');
@@ -515,21 +524,30 @@ const storage = multer.diskStorage({
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
// Phase 6.7: exact-match MIME + extension allowlist. Substring-based regex
// matchers (the previous /jpeg|png|.../ approach) accepted MIMEs like
// `application/jpeg-payload` because of partial matches; this rejects them.
const ALLOWED_MIME_TYPES = new Set([
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/tiff',
]);
const ALLOWED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tif', '.tiff']);
const upload = multer({
storage: storage,
limits: {
fileSize: MAX_UPLOAD_BYTES,
files: 1,
},
fileFilter: function (req, file, cb) {
// Accept only image files
const filetypes = /jpeg|jpg|png|gif|webp|tiff?/;
const mimetype = filetypes.test(file.mimetype);
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
if (mimetype && extname) {
return cb(null, true);
const ext = path.extname(file.originalname).toLowerCase();
if (!ALLOWED_MIME_TYPES.has(file.mimetype) || !ALLOWED_EXTENSIONS.has(ext)) {
return cb(new Error('Only image files are allowed (jpg, png, gif, webp, tiff)'));
}
cb(new Error('Only image files are allowed'));
cb(null, true);
}
});
@@ -647,7 +665,7 @@ async function setupSshTunnel() {
port: process.env.PROD_SSH_PORT || 22,
username: process.env.PROD_SSH_USER,
privateKey: process.env.PROD_SSH_KEY_PATH
? require('fs').readFileSync(process.env.PROD_SSH_KEY_PATH)
? fs.readFileSync(process.env.PROD_SSH_KEY_PATH)
: undefined,
compress: true
};
@@ -2872,4 +2890,4 @@ router.get('/query-products', async (req, res) => {
}
});
module.exports = router;
export default router;