396 lines
11 KiB
JavaScript
396 lines
11 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
// Create reusable uploads directory if it doesn't exist
|
|
const uploadsDir = path.join('/var/www/html/inventory/uploads/reusable');
|
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
|
|
// Configure multer for file uploads
|
|
const storage = multer.diskStorage({
|
|
destination: function (req, file, cb) {
|
|
console.log(`Saving reusable image to: ${uploadsDir}`);
|
|
cb(null, uploadsDir);
|
|
},
|
|
filename: function (req, file, cb) {
|
|
// Create unique filename with original extension
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
|
|
// Make sure we preserve the original file extension
|
|
let fileExt = path.extname(file.originalname).toLowerCase();
|
|
|
|
// Ensure there is a proper extension based on mimetype if none exists
|
|
if (!fileExt) {
|
|
switch (file.mimetype) {
|
|
case 'image/jpeg': fileExt = '.jpg'; break;
|
|
case 'image/png': fileExt = '.png'; break;
|
|
case 'image/gif': fileExt = '.gif'; break;
|
|
case 'image/webp': fileExt = '.webp'; break;
|
|
default: fileExt = '.jpg'; // Default to jpg
|
|
}
|
|
}
|
|
|
|
const fileName = `reusable-${uniqueSuffix}${fileExt}`;
|
|
console.log(`Generated filename: ${fileName} with mimetype: ${file.mimetype}`);
|
|
cb(null, fileName);
|
|
}
|
|
});
|
|
|
|
const upload = multer({
|
|
storage: storage,
|
|
limits: {
|
|
fileSize: 5 * 1024 * 1024, // 5MB max file size
|
|
},
|
|
fileFilter: function (req, file, cb) {
|
|
// Accept only image files
|
|
const filetypes = /jpeg|jpg|png|gif|webp/;
|
|
const mimetype = filetypes.test(file.mimetype);
|
|
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
|
|
|
|
if (mimetype && extname) {
|
|
return cb(null, true);
|
|
}
|
|
cb(new Error('Only image files are allowed'));
|
|
}
|
|
});
|
|
|
|
// Get all reusable images
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const pool = req.app.locals.pool;
|
|
if (!pool) {
|
|
throw new Error('Database pool not initialized');
|
|
}
|
|
|
|
const result = await pool.query(`
|
|
SELECT * FROM reusable_images
|
|
ORDER BY created_at DESC
|
|
`);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
console.error('Error fetching reusable images:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to fetch reusable images',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get images by company or global images
|
|
router.get('/by-company/:companyId', async (req, res) => {
|
|
try {
|
|
const { companyId } = req.params;
|
|
const pool = req.app.locals.pool;
|
|
if (!pool) {
|
|
throw new Error('Database pool not initialized');
|
|
}
|
|
|
|
// Get images that are either global or belong to this company
|
|
const result = await pool.query(`
|
|
SELECT * FROM reusable_images
|
|
WHERE is_global = true OR company = $1
|
|
ORDER BY created_at DESC
|
|
`, [companyId]);
|
|
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
console.error('Error fetching reusable images by company:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to fetch reusable images by company',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get global images only
|
|
router.get('/global', async (req, res) => {
|
|
try {
|
|
const pool = req.app.locals.pool;
|
|
if (!pool) {
|
|
throw new Error('Database pool not initialized');
|
|
}
|
|
|
|
const result = await pool.query(`
|
|
SELECT * FROM reusable_images
|
|
WHERE is_global = true
|
|
ORDER BY created_at DESC
|
|
`);
|
|
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
console.error('Error fetching global reusable images:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to fetch global reusable images',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get a single image by ID
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const pool = req.app.locals.pool;
|
|
if (!pool) {
|
|
throw new Error('Database pool not initialized');
|
|
}
|
|
|
|
const result = await pool.query(`
|
|
SELECT * FROM reusable_images
|
|
WHERE id = $1
|
|
`, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Reusable image not found' });
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
console.error('Error fetching reusable image:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to fetch reusable image',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Upload a new reusable image
|
|
router.post('/upload', upload.single('image'), async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No image file provided' });
|
|
}
|
|
|
|
const { name, is_global, company } = req.body;
|
|
|
|
// Validate required fields
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Image name is required' });
|
|
}
|
|
|
|
// Convert is_global from string to boolean
|
|
const isGlobal = is_global === 'true' || is_global === true;
|
|
|
|
// Validate company is provided for non-global images
|
|
if (!isGlobal && !company) {
|
|
return res.status(400).json({ error: 'Company is required for non-global images' });
|
|
}
|
|
|
|
// Log file information
|
|
console.log('Reusable image uploaded:', {
|
|
filename: req.file.filename,
|
|
originalname: req.file.originalname,
|
|
mimetype: req.file.mimetype,
|
|
size: req.file.size,
|
|
path: req.file.path
|
|
});
|
|
|
|
// Ensure the file exists
|
|
const filePath = path.join(uploadsDir, req.file.filename);
|
|
if (!fs.existsSync(filePath)) {
|
|
return res.status(500).json({ error: 'File was not saved correctly' });
|
|
}
|
|
|
|
// Create URL for the uploaded file
|
|
const baseUrl = 'https://inventory.acot.site';
|
|
const imageUrl = `${baseUrl}/uploads/reusable/${req.file.filename}`;
|
|
|
|
const pool = req.app.locals.pool;
|
|
if (!pool) {
|
|
throw new Error('Database pool not initialized');
|
|
}
|
|
|
|
// Insert record into database
|
|
const result = await pool.query(`
|
|
INSERT INTO reusable_images (
|
|
name,
|
|
filename,
|
|
file_path,
|
|
image_url,
|
|
is_global,
|
|
company,
|
|
mime_type,
|
|
file_size
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING *
|
|
`, [
|
|
name,
|
|
req.file.filename,
|
|
filePath,
|
|
imageUrl,
|
|
isGlobal,
|
|
isGlobal ? null : company,
|
|
req.file.mimetype,
|
|
req.file.size
|
|
]);
|
|
|
|
// Return success response with image data
|
|
res.status(201).json({
|
|
success: true,
|
|
image: result.rows[0],
|
|
message: 'Image uploaded successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error uploading reusable image:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to upload image' });
|
|
}
|
|
});
|
|
|
|
// Update image details (name, is_global, company)
|
|
router.put('/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { name, is_global, company } = req.body;
|
|
|
|
// Validate required fields
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Image name is required' });
|
|
}
|
|
|
|
// Convert is_global from string to boolean if necessary
|
|
const isGlobal = typeof is_global === 'string' ? is_global === 'true' : !!is_global;
|
|
|
|
// Validate company is provided for non-global images
|
|
if (!isGlobal && !company) {
|
|
return res.status(400).json({ error: 'Company is required for non-global images' });
|
|
}
|
|
|
|
const pool = req.app.locals.pool;
|
|
if (!pool) {
|
|
throw new Error('Database pool not initialized');
|
|
}
|
|
|
|
// Check if the image exists
|
|
const checkResult = await pool.query('SELECT * FROM reusable_images WHERE id = $1', [id]);
|
|
if (checkResult.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Reusable image not found' });
|
|
}
|
|
|
|
const result = await pool.query(`
|
|
UPDATE reusable_images
|
|
SET
|
|
name = $1,
|
|
is_global = $2,
|
|
company = $3
|
|
WHERE id = $4
|
|
RETURNING *
|
|
`, [
|
|
name,
|
|
isGlobal,
|
|
isGlobal ? null : company,
|
|
id
|
|
]);
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (error) {
|
|
console.error('Error updating reusable image:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to update reusable image',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Delete a reusable image
|
|
router.delete('/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const pool = req.app.locals.pool;
|
|
if (!pool) {
|
|
throw new Error('Database pool not initialized');
|
|
}
|
|
|
|
// Get the image data first to get the filename
|
|
const imageResult = await pool.query('SELECT * FROM reusable_images WHERE id = $1', [id]);
|
|
|
|
if (imageResult.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Reusable image not found' });
|
|
}
|
|
|
|
const image = imageResult.rows[0];
|
|
|
|
// Delete from database
|
|
await pool.query('DELETE FROM reusable_images WHERE id = $1', [id]);
|
|
|
|
// Delete the file from filesystem
|
|
const filePath = path.join(uploadsDir, image.filename);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
|
|
res.json({
|
|
message: 'Reusable image deleted successfully',
|
|
image
|
|
});
|
|
} catch (error) {
|
|
console.error('Error deleting reusable image:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to delete reusable image',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Check if file exists and permissions
|
|
router.get('/check-file/:filename', (req, res) => {
|
|
const { filename } = req.params;
|
|
|
|
// Prevent directory traversal
|
|
if (filename.includes('..') || filename.includes('/')) {
|
|
return res.status(400).json({ error: 'Invalid filename' });
|
|
}
|
|
|
|
const filePath = path.join(uploadsDir, filename);
|
|
|
|
try {
|
|
// Check if file exists
|
|
if (!fs.existsSync(filePath)) {
|
|
return res.status(404).json({
|
|
error: 'File not found',
|
|
path: filePath,
|
|
exists: false,
|
|
readable: false
|
|
});
|
|
}
|
|
|
|
// Check if file is readable
|
|
fs.accessSync(filePath, fs.constants.R_OK);
|
|
|
|
// Get file stats
|
|
const stats = fs.statSync(filePath);
|
|
|
|
return res.json({
|
|
filename,
|
|
path: filePath,
|
|
exists: true,
|
|
readable: true,
|
|
isFile: stats.isFile(),
|
|
isDirectory: stats.isDirectory(),
|
|
size: stats.size,
|
|
created: stats.birthtime,
|
|
modified: stats.mtime,
|
|
permissions: stats.mode.toString(8)
|
|
});
|
|
} catch (error) {
|
|
return res.status(500).json({
|
|
error: error.message,
|
|
path: filePath,
|
|
exists: fs.existsSync(filePath),
|
|
readable: false
|
|
});
|
|
}
|
|
});
|
|
|
|
// Error handling middleware
|
|
router.use((err, req, res, next) => {
|
|
console.error('Reusable images route error:', err);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
details: err.message
|
|
});
|
|
});
|
|
|
|
module.exports = router;
|