diff --git a/inventory-server/package-lock.json b/inventory-server/package-lock.json index 7c959f4..a0e42a4 100755 --- a/inventory-server/package-lock.json +++ b/inventory-server/package-lock.json @@ -16,6 +16,7 @@ "multer": "^1.4.5-lts.1", "mysql2": "^3.12.0", "pm2": "^5.3.0", + "ssh2": "^1.16.0", "uuid": "^9.0.1" }, "devDependencies": { @@ -367,6 +368,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -416,6 +426,15 @@ "node": ">=10.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -499,6 +518,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -703,6 +731,20 @@ "node": ">= 0.10" } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/croner": { "version": "4.1.97", "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", @@ -1758,6 +1800,13 @@ "node": ">=12.0.0" } }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, "node_modules/needle": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", @@ -2816,6 +2865,23 @@ "node": ">= 0.6" } }, + "node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2954,6 +3020,12 @@ "node": ">= 0.8.0" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/tx2": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", diff --git a/inventory-server/package.json b/inventory-server/package.json index 16071ce..e776547 100755 --- a/inventory-server/package.json +++ b/inventory-server/package.json @@ -25,6 +25,7 @@ "multer": "^1.4.5-lts.1", "mysql2": "^3.12.0", "pm2": "^5.3.0", + "ssh2": "^1.16.0", "uuid": "^9.0.1" }, "devDependencies": { diff --git a/inventory-server/scripts/test-prod-connection.js b/inventory-server/scripts/test-prod-connection.js new file mode 100644 index 0000000..d83216f --- /dev/null +++ b/inventory-server/scripts/test-prod-connection.js @@ -0,0 +1,89 @@ +const mysql = require('mysql2/promise'); +const { Client } = require('ssh2'); +const dotenv = require('dotenv'); +const path = require('path'); + +dotenv.config({ path: path.join(__dirname, '../.env') }); + +// SSH configuration +const sshConfig = { + host: process.env.PROD_SSH_HOST, + 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) : undefined +}; + +// Database configuration +const dbConfig = { + host: process.env.PROD_DB_HOST || 'localhost', // Usually localhost when tunneling + user: process.env.PROD_DB_USER, + password: process.env.PROD_DB_PASSWORD, + database: process.env.PROD_DB_NAME, + port: process.env.PROD_DB_PORT || 3306 +}; + +async function testConnection() { + const ssh = new Client(); + + try { + // Create new Promise for SSH connection + await new Promise((resolve, reject) => { + ssh.on('ready', resolve) + .on('error', reject) + .connect(sshConfig); + }); + + console.log('SSH Connection successful!'); + + // Forward local port to remote MySQL port + const tunnel = await new Promise((resolve, reject) => { + ssh.forwardOut( + '127.0.0.1', + 0, + dbConfig.host, + dbConfig.port, + (err, stream) => { + if (err) reject(err); + resolve(stream); + } + ); + }); + + console.log('Port forwarding established'); + + // Create MySQL connection over SSH tunnel + const connection = await mysql.createConnection({ + ...dbConfig, + stream: tunnel + }); + + console.log('MySQL Connection successful!'); + + // Test query + const [rows] = await connection.query('SELECT COUNT(*) as count FROM products'); + console.log('Query successful! Product count:', rows[0].count); + + // Clean up + await connection.end(); + ssh.end(); + console.log('Connections closed successfully'); + return rows[0].count; + + } catch (error) { + console.error('Error:', error); + if (ssh) ssh.end(); + throw error; + } +} + +// If running directly (not imported) +if (require.main === module) { + testConnection() + .then(() => process.exit(0)) + .catch(error => { + console.error('Test failed:', error); + process.exit(1); + }); +} + +module.exports = { testConnection }; \ No newline at end of file diff --git a/inventory-server/src/routes/test-connection.js b/inventory-server/src/routes/test-connection.js new file mode 100644 index 0000000..633963b --- /dev/null +++ b/inventory-server/src/routes/test-connection.js @@ -0,0 +1,22 @@ +const express = require('express'); +const router = express.Router(); +const { testConnection } = require('../../scripts/test-prod-connection'); + +router.get('/test-prod-connection', async (req, res) => { + try { + const productCount = await testConnection(); + res.json({ + success: true, + message: 'Successfully connected to production database', + productCount + }); + } catch (error) { + console.error('Production connection test failed:', error); + res.status(500).json({ + success: false, + error: error.message || 'Failed to connect to production database' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/inventory-server/src/server.js b/inventory-server/src/server.js index 1d31b02..c58ad7d 100755 --- a/inventory-server/src/server.js +++ b/inventory-server/src/server.js @@ -16,6 +16,7 @@ const configRouter = require('./routes/config'); const metricsRouter = require('./routes/metrics'); const vendorsRouter = require('./routes/vendors'); const categoriesRouter = require('./routes/categories'); +const testConnectionRouter = require('./routes/test-connection'); // Get the absolute path to the .env file const envPath = path.resolve(process.cwd(), '.env'); @@ -91,6 +92,7 @@ app.use('/api/config', configRouter); app.use('/api/metrics', metricsRouter); app.use('/api/vendors', vendorsRouter); app.use('/api/categories', categoriesRouter); +app.use('/api', testConnectionRouter); // Basic health check route app.get('/health', (req, res) => { diff --git a/inventory/src/components/settings/DataManagement.tsx b/inventory/src/components/settings/DataManagement.tsx index a3c49d1..11a223e 100644 --- a/inventory/src/components/settings/DataManagement.tsx +++ b/inventory/src/components/settings/DataManagement.tsx @@ -13,7 +13,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Loader2, RefreshCw, Upload, X } from "lucide-react"; +import { Loader2, RefreshCw, Upload, X, Database } from "lucide-react"; import config from '../../config'; import { toast } from "sonner"; @@ -71,6 +71,9 @@ export function DataManagement() { // Track cancellation state const [cancelledOperations, setCancelledOperations] = useState>(new Set()); + // Add new state for testing connection + const [isTestingConnection, setIsTestingConnection] = useState(false); + // Helper to check if any operation is running const isAnyOperationRunning = () => { return isUpdating || isImporting || isResetting || isResettingMetrics || isCalculatingMetrics; @@ -829,8 +832,56 @@ export function DataManagement() { } }; + const handleTestConnection = async () => { + setIsTestingConnection(true); + try { + const response = await fetch(`${config.apiUrl}/test-prod-connection`, { + credentials: 'include' + }); + + const data = await response.json(); + + if (response.ok) { + toast.success(`Successfully connected to production database. Found ${data.productCount.toLocaleString()} products.`); + } else { + throw new Error(data.error || 'Failed to connect to production database'); + } + } catch (error) { + toast.error(`Connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { + setIsTestingConnection(false); + } + }; + return (
+ {/* Test Production Connection Card */} + + + Test Production Connection + Verify connection to production database + + + + + + {/* Update CSV Card */}