🔒 Improve SSH connection handling, enhance file transfer reliability, and update OAuth error handling and tests

This commit is contained in:
2026-01-30 04:53:18 +00:00
parent ab595394be
commit 9fdfbb22d8
14 changed files with 218 additions and 103 deletions

View File

@@ -34,15 +34,13 @@ const SSH_HOSTS = {
host: '10.0.0.81',
port: 2323,
username: 'GregRJacobs',
privateKeyPath: '~/.ssh/ds3627xs_gregrjacobs',
password: 'J@Cubs88'
privateKeyPath: '~/.ssh/ds3627xs_gregrjacobs'
},
external: {
host: 'home.gregrjacobs.com',
port: 2323,
username: 'GregRJacobs',
privateKeyPath: '~/.ssh/ds3627xs_gregrjacobs',
password: 'J@Cubs88'
privateKeyPath: '~/.ssh/ds3627xs_gregrjacobs'
}
};
@@ -144,7 +142,8 @@ function createModifiedDockerCompose(port, sslPort, backendPort) {
);
} else {
// Remove HTTPS port mapping line if no SSL port specified
content = content.replace(/\s*- ['"](\d+):443['"]\n?/g, '');
// Make sure to preserve newline structure
content = content.replace(/\n\s*- ['"](\d+):443['"]/g, '');
}
// Replace backend port mapping
@@ -220,17 +219,49 @@ async function deploy() {
}
console.log(`🔌 Backend Port: ${config.backendPort}`);
// Connect to Synology
// Connect to Synology using ~/.ssh/config
console.log('\n🔐 Connecting to Synology...');
await ssh.connect({
host: sshConfig.host,
port: sshConfig.port,
username: sshConfig.username,
privateKeyPath: expandTilde(sshConfig.privateKeyPath),
password: sshConfig.password,
tryKeyboard: true
});
console.log('✅ Connected successfully');
const keyPath = expandTilde(sshConfig.privateKeyPath);
console.log(` 🔑 Using SSH key: ${keyPath}`);
console.log(` 📍 Target: ${sshConfig.username}@${sshConfig.host}:${sshConfig.port}`);
// Verify key file exists
if (!fs.existsSync(keyPath)) {
throw new Error(`SSH key file not found: ${keyPath}`);
}
try {
const privateKeyContent = fs.readFileSync(keyPath, 'utf8');
const keySize = privateKeyContent.length;
console.log(` 📂 Key file size: ${keySize} bytes`);
if (keySize === 0) {
throw new Error('SSH key file is empty');
}
// Use node-ssh with private key directly
await ssh.connect({
host: sshConfig.host,
port: sshConfig.port,
username: sshConfig.username,
privateKey: privateKeyContent,
readyTimeout: 60000,
tryKeyboard: false
});
console.log('✅ Connected successfully');
} catch (connError) {
console.error('\n❌ SSH Connection Failed');
console.error(`Error: ${connError.message}`);
console.error('\nPossible causes:');
console.error('1. SSH public key not added to ~/.ssh/authorized_keys on the server');
console.error('2. SSH key has wrong permissions (should be 600)');
console.error('3. SSH user home directory permissions are wrong');
console.error('\nVerify the key works manually:');
console.error(` ssh -i ${keyPath} ${sshConfig.username}@${sshConfig.host} -p ${sshConfig.port} "whoami"`);
console.error('\nIf that fails, the public key needs to be added on the server:');
console.error(` cat ~/.ssh/${path.basename(keyPath)}.pub | ssh ${sshConfig.username}@${sshConfig.host} -p ${sshConfig.port} "cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"`);
throw new Error(`SSH connection failed: ${connError.message}`);
}
// Build Vue 3 application
console.log('\n🔨 Building Vue 3 application...');
@@ -315,94 +346,89 @@ async function deploy() {
// First transfer the dist directory
console.log(' 📦 Transferring dist directory...');
const distFiles = [];
function getDistFiles(dir, baseDir = DIST_DIR) {
// Count files for reporting
let fileCount = 0;
function countFiles(dir) {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
getDistFiles(fullPath, baseDir);
countFiles(fullPath);
} else {
const relativePath = path.relative(baseDir, fullPath);
distFiles.push({
local: fullPath,
remote: `${REMOTE_PATH}/dist/${relativePath.replace(/\\/g, '/')}`
});
fileCount++;
}
}
}
getDistFiles(DIST_DIR);
console.log(` Found ${distFiles.length} files in dist/`);
countFiles(DIST_DIR);
console.log(` Found ${fileCount} files in dist/`);
// Create dist directory on remote
await ssh.execCommand(`mkdir -p ${REMOTE_PATH}/dist`);
// Transfer dist files
let transferred = 0;
for (const file of distFiles) {
// Transfer dist directory using rsync
try {
console.log(` 📡 Transferring dist directory via rsync...`);
const { execSync } = await import('child_process');
const expandedKeyPath = expandTilde(sshConfig.privateKeyPath);
// Use rsync with SSH for reliable transfer
// Key options:
// - IdentitiesOnly=yes: Only use specified key, not ssh-agent keys
// - rsync-path: Ensure we use the correct rsync binary on remote
const rsyncCmd = `rsync -av --delete -e "ssh -p ${sshConfig.port} -i ${expandedKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes" "${DIST_DIR}/" "${sshConfig.username}@${sshConfig.host}:${REMOTE_PATH}/dist/" --rsync-path="/usr/bin/rsync"`;
try {
// Create remote directory for this file
const remoteDir = path.dirname(file.remote);
await ssh.execCommand(`mkdir -p ${remoteDir}`);
await ssh.putFile(file.local, file.remote);
transferred++;
if (transferred % 10 === 0) {
console.log(
` 📁 Transferred ${transferred}/${distFiles.length} files...`
);
}
} catch (error) {
console.log(
` ⚠️ Failed to transfer ${path.relative(DIST_DIR, file.local)}: ${error.message}`
);
execSync(rsyncCmd, {
stdio: 'inherit',
shell: '/bin/bash'
});
console.log(` ✅ Transferred dist directory successfully via rsync`);
} catch (rsyncError) {
console.log(` ❌ Rsync failed: ${rsyncError.message}`);
throw rsyncError;
}
} catch (transferError) {
console.log(` ❌ File transfer failed: ${transferError.message}`);
throw new Error(`File transfer failed: ${transferError.message}`);
}
console.log(` ✅ Transferred ${transferred} files from dist/`);
// Transfer backend server files
console.log(' 📦 Transferring backend server files...');
const serverDir = path.join(SOURCE_DIR, 'server');
await ssh.execCommand(`mkdir -p ${REMOTE_PATH}/server`);
// Transfer server package files
const serverFiles = [
'package.json',
'oauth-proxy.js',
'gamemaster-api.js',
'.env.example'
];
for (const file of serverFiles) {
const localPath = path.join(serverDir, file);
const remotePath = `${REMOTE_PATH}/server/${file}`;
if (fs.existsSync(localPath)) {
await ssh.putFile(localPath, remotePath);
console.log(` ✅ server/${file}`);
// Use rsync for server files with exclusions
try {
console.log(` 📡 Transferring server files via rsync...`);
const { execSync } = await import('child_process');
const expandedKeyPath = expandTilde(sshConfig.privateKeyPath);
// Use rsync with SSH for reliable transfer and exclusions
// Key options:
// - IdentitiesOnly=yes: Only use specified key, not ssh-agent keys
// - rsync-path: Ensure we use the correct rsync binary on remote
const rsyncCmd = `rsync -av --delete --exclude='node_modules' --exclude='tests' --exclude='.git' --exclude='dist' --exclude='build' -e "ssh -p ${sshConfig.port} -i ${expandedKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes" "${serverDir}/" "${sshConfig.username}@${sshConfig.host}:${REMOTE_PATH}/server/" --rsync-path="/usr/bin/rsync"`;
try {
execSync(rsyncCmd, {
stdio: 'inherit',
shell: '/bin/bash'
});
console.log(` ✅ Backend files transferred successfully via rsync`);
} catch (rsyncError) {
console.log(` ❌ Rsync failed: ${rsyncError.message}`);
throw rsyncError;
}
} catch (transferError) {
console.log(` ❌ File transfer failed: ${transferError.message}`);
throw new Error(`Backend file transfer failed: ${transferError.message}`);
}
// Transfer server subdirectories
const serverDirs = ['middleware', 'routes', 'utils', 'data'];
for (const dir of serverDirs) {
const localDir = path.join(serverDir, dir);
if (fs.existsSync(localDir)) {
await ssh.execCommand(`mkdir -p ${REMOTE_PATH}/server/${dir}`);
const files = fs.readdirSync(localDir, { withFileTypes: true });
for (const file of files) {
if (file.isFile()) {
const localFile = path.join(localDir, file.name);
const remoteFile = `${REMOTE_PATH}/server/${dir}/${file.name}`;
await ssh.putFile(localFile, remoteFile);
}
}
console.log(` ✅ server/${dir}/ (${files.length} files)`);
}
}
console.log(` ✅ Backend files transferred`);
// Now transfer config files
console.log(' 📦 Transferring configuration files...');
const filesToTransfer = [
{
local: path.join(SOURCE_DIR, 'Dockerfile.frontend'),
@@ -422,29 +448,24 @@ async function deploy() {
}
];
// Transfer config files using cat over SSH (more reliable than SFTP)
for (const file of filesToTransfer) {
const fileName = path.basename(file.local);
try {
await ssh.putFile(file.local, file.remote);
console.log(`${path.basename(file.local)}`);
} catch {
// If SFTP fails, fall back to cat method
console.log(
` ⚠️ SFTP failed for ${path.basename(file.local)}, using cat fallback...`
);
const fileContent = fs.readFileSync(file.local, 'utf8');
const catResult = await ssh.execCommand(
`cat > '${file.remote}' << 'EOFMARKER'\n${fileContent}\nEOFMARKER`
);
if (catResult.stdout) console.log(` Output: ${catResult.stdout}`);
if (catResult.stderr) console.log(` Stderr: ${catResult.stderr}`);
if (catResult.code !== 0) {
throw new Error(
`Failed to transfer ${path.basename(file.local)}: ${catResult.stderr}`
`Failed to transfer ${fileName}: ${catResult.stderr}`
);
}
console.log(
`${path.basename(file.local)} (${fs.statSync(file.local).size} bytes)`
`${fileName} (${fs.statSync(file.local).size} bytes)`
);
} catch (error) {
throw new Error(`Failed to transfer ${fileName}: ${error.message}`);
}
}