Fix csv update/import on settings page + lots of cors work

This commit is contained in:
2025-01-10 14:17:07 -05:00
parent dbdf77331c
commit a1f4e57394
9 changed files with 957 additions and 329 deletions

View File

@@ -1,115 +0,0 @@
const express = require('express');
const cors = require('cors');
const mysql = require('mysql2/promise');
const productsRouter = require('./routes/products');
const dashboardRouter = require('./routes/dashboard');
const ordersRouter = require('./routes/orders');
const csvRoutes = require('./routes/csv');
const app = express();
// Debug middleware to log all requests
app.use((req, res, next) => {
console.log(`[App Debug] ${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
// Configure CORS with specific options
app.use(cors({
origin: [
'http://localhost:5173', // Local development
'https://inventory.kent.pw', // Production frontend
/\.kent\.pw$/ // Any subdomain of kent.pw
],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
optionsSuccessStatus: 200
}));
app.use(express.json());
// Database connection
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'inventory',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Make db pool available in routes
app.locals.pool = pool;
// Debug endpoint to list all registered routes
app.get('/api/debug/routes', (req, res) => {
console.log('Debug routes endpoint hit');
const routes = [];
app._router.stack.forEach(middleware => {
if (middleware.route) {
routes.push({
path: middleware.route.path,
methods: Object.keys(middleware.route.methods)
});
} else if (middleware.name === 'router') {
middleware.handle.stack.forEach(handler => {
if (handler.route) {
const fullPath = (middleware.regexp.source === '^\\/?(?=\\/|$)' ? '' : middleware.regexp.source.replace(/\\\//g, '/').replace(/\^|\$/g, '')) + handler.route.path;
routes.push({
path: fullPath,
methods: Object.keys(handler.route.methods)
});
}
});
}
});
res.json(routes);
});
// Test endpoint to verify server is running
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Mount all routes under /api
console.log('Mounting routes...');
console.log('Mounting products routes...');
app.use('/api/products', productsRouter);
console.log('Mounting dashboard routes...');
app.use('/api/dashboard', dashboardRouter);
console.log('Mounting orders routes...');
app.use('/api/orders', ordersRouter);
console.log('Mounting CSV routes...');
app.use('/api/csv', csvRoutes);
console.log('CSV routes mounted');
console.log('All routes mounted');
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({ error: err.message });
});
// 404 handler
app.use((req, res) => {
console.log('404 Not Found:', req.method, req.path);
res.status(404).json({ error: 'Not Found' });
});
const PORT = process.env.PORT || 3010;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
console.log('Available routes:');
console.log('- GET /api/health');
console.log('- GET /api/debug/routes');
console.log('- GET /api/csv/status');
console.log('- GET /api/csv/test');
console.log('- POST /api/csv/update');
});

View File

@@ -0,0 +1,39 @@
const cors = require('cors');
// Single CORS middleware for all endpoints
const corsMiddleware = cors({
origin: [
'https://inventory.kent.pw',
'http://localhost:5173',
/^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/,
/^http:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/
],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['Content-Type'],
credentials: true
});
// Error handler for CORS
const corsErrorHandler = (err, req, res, next) => {
if (err.message === 'CORS not allowed') {
console.error('CORS Error:', {
origin: req.get('Origin'),
method: req.method,
path: req.path,
headers: req.headers
});
res.status(403).json({
error: 'CORS not allowed',
origin: req.get('Origin'),
message: 'Origin not in allowed list: https://inventory.kent.pw, localhost:5173, 192.168.x.x, or 10.x.x.x'
});
} else {
next(err);
}
};
module.exports = {
corsMiddleware,
corsErrorHandler
};

View File

@@ -14,15 +14,77 @@ let activeImport = null;
let importProgress = null;
// SSE clients for progress updates
const clients = new Set();
const updateClients = new Set();
const importClients = new Set();
// Helper to send progress to all connected clients
function sendProgressToClients(progress) {
// Helper to send progress to specific clients
function sendProgressToClients(clients, progress) {
const data = typeof progress === 'string' ? { progress } : progress;
// Ensure we have a status field
if (!data.status) {
data.status = 'running';
}
const message = `data: ${JSON.stringify(data)}\n\n`;
clients.forEach(client => {
client.write(`data: ${JSON.stringify(progress)}\n\n`);
try {
client.write(message);
// Immediately flush the response
if (typeof client.flush === 'function') {
client.flush();
}
} catch (error) {
// Silently remove failed client
clients.delete(client);
}
});
}
// Progress endpoints
router.get('/update/progress', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Credentials': 'true'
});
// Send an initial message to test the connection
res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n');
// Add this client to the update set
updateClients.add(res);
// Remove client when connection closes
req.on('close', () => {
updateClients.delete(res);
});
});
router.get('/import/progress', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Credentials': 'true'
});
// Send an initial message to test the connection
res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n');
// Add this client to the import set
importClients.add(res);
// Remove client when connection closes
req.on('close', () => {
importClients.delete(res);
});
});
// Debug endpoint to verify route registration
router.get('/test', (req, res) => {
console.log('CSV test endpoint hit');
@@ -39,45 +101,72 @@ router.get('/status', (req, res) => {
});
// Route to update CSV files
router.post('/update', async (req, res) => {
console.log('CSV update endpoint hit');
router.post('/update', async (req, res, next) => {
if (activeImport) {
console.log('Import already in progress');
return res.status(409).json({ error: 'Import already in progress' });
}
try {
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'update-csv.js');
console.log('Running script:', scriptPath);
if (!require('fs').existsSync(scriptPath)) {
console.error('Script not found:', scriptPath);
return res.status(500).json({ error: 'Update script not found' });
}
activeImport = spawn('node', [scriptPath]);
activeImport.stdout.on('data', (data) => {
console.log(`CSV Update: ${data}`);
importProgress = data.toString();
sendProgressToClients({ status: 'running', progress: importProgress });
const output = data.toString().trim();
try {
// Try to parse as JSON
const jsonData = JSON.parse(output);
sendProgressToClients(updateClients, {
status: 'running',
...jsonData
});
} catch (e) {
// If not JSON, send as plain progress
sendProgressToClients(updateClients, {
status: 'running',
progress: output
});
}
});
activeImport.stderr.on('data', (data) => {
console.error(`CSV Update Error: ${data}`);
sendProgressToClients({ status: 'error', error: data.toString() });
const error = data.toString().trim();
try {
// Try to parse as JSON
const jsonData = JSON.parse(error);
sendProgressToClients(updateClients, {
status: 'error',
...jsonData
});
} catch {
sendProgressToClients(updateClients, {
status: 'error',
error
});
}
});
await new Promise((resolve, reject) => {
activeImport.on('close', (code) => {
console.log(`CSV update process exited with code ${code}`);
if (code === 0) {
sendProgressToClients({ status: 'complete' });
// Don't treat cancellation (code 143/SIGTERM) as an error
if (code === 0 || code === 143) {
sendProgressToClients(updateClients, {
status: 'complete',
operation: code === 143 ? 'Operation cancelled' : 'Update complete'
});
resolve();
} else {
sendProgressToClients({ status: 'error', error: `Process exited with code ${code}` });
reject(new Error(`Update process exited with code ${code}`));
const errorMsg = `Update process exited with code ${code}`;
sendProgressToClients(updateClients, {
status: 'error',
error: errorMsg
});
reject(new Error(errorMsg));
}
activeImport = null;
importProgress = null;
@@ -89,7 +178,138 @@ router.post('/update', async (req, res) => {
console.error('Error updating CSV files:', error);
activeImport = null;
importProgress = null;
res.status(500).json({ error: 'Failed to update CSV files', details: error.message });
sendProgressToClients(updateClients, {
status: 'error',
error: error.message
});
next(error);
}
});
// Route to import CSV files
router.post('/import', async (req, res) => {
if (activeImport) {
return res.status(409).json({ error: 'Import already in progress' });
}
try {
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'import-csv.js');
if (!require('fs').existsSync(scriptPath)) {
return res.status(500).json({ error: 'Import script not found' });
}
// Get test limits from request body
const { products = 0, orders = 10000, purchaseOrders = 10000 } = req.body;
// Create environment variables for the script
const env = {
...process.env,
PRODUCTS_TEST_LIMIT: products.toString(),
ORDERS_TEST_LIMIT: orders.toString(),
PURCHASE_ORDERS_TEST_LIMIT: purchaseOrders.toString()
};
activeImport = spawn('node', [scriptPath], { env });
activeImport.stdout.on('data', (data) => {
const output = data.toString().trim();
try {
// Try to parse as JSON
const jsonData = JSON.parse(output);
sendProgressToClients(importClients, {
status: 'running',
...jsonData
});
} catch {
// If not JSON, send as plain progress
sendProgressToClients(importClients, {
status: 'running',
progress: output
});
}
});
activeImport.stderr.on('data', (data) => {
const error = data.toString().trim();
try {
// Try to parse as JSON
const jsonData = JSON.parse(error);
sendProgressToClients(importClients, {
status: 'error',
...jsonData
});
} catch {
sendProgressToClients(importClients, {
status: 'error',
error
});
}
});
await new Promise((resolve, reject) => {
activeImport.on('close', (code) => {
// Don't treat cancellation (code 143/SIGTERM) as an error
if (code === 0 || code === 143) {
sendProgressToClients(importClients, {
status: 'complete',
operation: code === 143 ? 'Operation cancelled' : 'Import complete'
});
resolve();
} else {
sendProgressToClients(importClients, {
status: 'error',
error: `Process exited with code ${code}`
});
reject(new Error(`Import process exited with code ${code}`));
}
activeImport = null;
importProgress = null;
});
});
res.json({ success: true });
} catch (error) {
console.error('Error importing CSV files:', error);
activeImport = null;
importProgress = null;
sendProgressToClients(importClients, {
status: 'error',
error: error.message
});
res.status(500).json({ error: 'Failed to import CSV files', details: error.message });
}
});
// Route to cancel active process
router.post('/cancel', (req, res) => {
if (!activeImport) {
return res.status(404).json({ error: 'No active process to cancel' });
}
try {
// Kill the process
activeImport.kill();
// Clean up
activeImport = null;
importProgress = null;
// Notify all clients
const cancelMessage = {
status: 'complete',
operation: 'Operation cancelled'
};
sendProgressToClients(updateClients, cancelMessage);
sendProgressToClients(importClients, cancelMessage);
res.json({ success: true });
} catch (error) {
// Even if there's an error, try to clean up
activeImport = null;
importProgress = null;
res.status(500).json({ error: 'Failed to cancel process' });
}
});

View File

@@ -1,5 +1,12 @@
const path = require('path');
const fs = require('fs');
const express = require('express');
const mysql = require('mysql2/promise');
const { corsMiddleware, corsErrorHandler } = require('./middleware/cors');
const productsRouter = require('./routes/products');
const dashboardRouter = require('./routes/dashboard');
const ordersRouter = require('./routes/orders');
const csvRouter = require('./routes/csv');
// Get the absolute path to the .env file
const envPath = path.resolve(process.cwd(), '.env');
@@ -21,23 +28,6 @@ try {
console.error('Error loading .env file:', error);
}
// Log environment variables (excluding sensitive data)
console.log('Environment variables loaded:', {
NODE_ENV: process.env.NODE_ENV,
PORT: process.env.PORT,
DB_HOST: process.env.DB_HOST,
DB_USER: process.env.DB_USER,
DB_NAME: process.env.DB_NAME,
// Not logging DB_PASSWORD for security
});
const express = require('express');
const cors = require('cors');
const mysql = require('mysql2/promise');
const productsRouter = require('./routes/products');
const dashboardRouter = require('./routes/dashboard');
const ordersRouter = require('./routes/orders');
// Ensure required directories exist
['logs', 'uploads'].forEach(dir => {
if (!fs.existsSync(dir)) {
@@ -47,39 +37,62 @@ const ordersRouter = require('./routes/orders');
const app = express();
// CORS configuration - move before route handlers
app.use(cors({
origin: ['https://inventory.kent.pw', 'https://www.inventory.kent.pw'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
optionsSuccessStatus: 200 // Some legacy browsers (IE11) choke on 204
}));
// Debug middleware to log request details
app.use((req, res, next) => {
console.log('Request details:', {
method: req.method,
url: req.url,
origin: req.get('Origin'),
headers: req.headers
});
next();
});
// Apply CORS middleware first, before any other middleware
app.use(corsMiddleware);
// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ${req.method} ${req.url} ${res.statusCode} ${duration}ms`
);
// Routes
app.use('/api/products', productsRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/orders', ordersRouter);
app.use('/api/csv', csvRouter);
// Basic health check route
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
next();
});
// Error handling middleware - move before route handlers
// CORS error handler - must be before other error handlers
app.use(corsErrorHandler);
// Error handling middleware - MUST be after routes and CORS error handler
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] Error:`, err);
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'An internal server error occurred'
: err.message
});
// Send detailed error in development, generic in production
const error = process.env.NODE_ENV === 'production'
? 'An internal server error occurred'
: err.message || err;
res.status(err.status || 500).json({ error });
});
// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(`[${new Date().toISOString()}] Uncaught Exception:`, err);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error(`[${new Date().toISOString()}] Unhandled Rejection at:`, promise, 'reason:', reason);
});
// Database connection pool
@@ -109,30 +122,6 @@ pool.getConnection()
process.exit(1);
});
// Routes
app.use('/api/products', productsRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/orders', ordersRouter);
// Basic health check route
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
});
// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(`[${new Date().toISOString()}] Uncaught Exception:`, err);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error(`[${new Date().toISOString()}] Unhandled Rejection at:`, promise, 'reason:', reason);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`[Server] Running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`);