diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js index 81717c5..e7a6c2e 100644 --- a/backend/src/routes/settingsRoutes.js +++ b/backend/src/routes/settingsRoutes.js @@ -124,7 +124,15 @@ router.put('/', authenticateToken, requireManageSettings, [ body('updateInterval').isInt({ min: 5, max: 1440 }).withMessage('Update interval must be between 5 and 1440 minutes'), body('autoUpdate').isBoolean().withMessage('Auto update must be a boolean'), body('githubRepoUrl').optional().isLength({ min: 1 }).withMessage('GitHub repo URL must be a non-empty string'), - body('sshKeyPath').optional().isLength({ min: 1 }).withMessage('SSH key path must be a non-empty string') + body('sshKeyPath').optional().custom((value) => { + if (value && value.trim().length === 0) { + return true; // Allow empty string + } + if (value && value.trim().length < 1) { + throw new Error('SSH key path must be a non-empty string'); + } + return true; + }) ], async (req, res) => { try { console.log('Settings update request body:', req.body); diff --git a/backend/src/routes/versionRoutes.js b/backend/src/routes/versionRoutes.js index a7b0f15..df68889 100644 --- a/backend/src/routes/versionRoutes.js +++ b/backend/src/routes/versionRoutes.js @@ -27,6 +27,118 @@ router.get('/current', authenticateToken, async (req, res) => { } }); +// Test SSH key permissions and GitHub access +router.post('/test-ssh-key', authenticateToken, requireManageSettings, async (req, res) => { + try { + const { sshKeyPath, githubRepoUrl } = req.body; + + if (!sshKeyPath || !githubRepoUrl) { + return res.status(400).json({ + error: 'SSH key path and GitHub repo URL are required' + }); + } + + // Parse repository info + let owner, repo; + if (githubRepoUrl.includes('git@github.com:')) { + const match = githubRepoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/); + if (match) { + [, owner, repo] = match; + } + } else if (githubRepoUrl.includes('github.com/')) { + const match = githubRepoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/); + if (match) { + [, owner, repo] = match; + } + } + + if (!owner || !repo) { + return res.status(400).json({ + error: 'Invalid GitHub repository URL format' + }); + } + + // Check if SSH key file exists and is readable + try { + require('fs').accessSync(sshKeyPath); + } catch (e) { + return res.status(400).json({ + error: 'SSH key file not found or not accessible', + details: `Cannot access: ${sshKeyPath}`, + suggestion: 'Check the file path and ensure the application has read permissions' + }); + } + + // Test SSH connection to GitHub + const sshRepoUrl = `git@github.com:${owner}/${repo}.git`; + const env = { + ...process.env, + GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o ConnectTimeout=10` + }; + + try { + // Test with a simple git command + const { stdout } = await execAsync( + `git ls-remote --heads ${sshRepoUrl} | head -n 1`, + { + timeout: 15000, + env: env + } + ); + + if (stdout.trim()) { + return res.json({ + success: true, + message: 'SSH key is working correctly', + details: { + sshKeyPath, + repository: `${owner}/${repo}`, + testResult: 'Successfully connected to GitHub' + } + }); + } else { + return res.status(400).json({ + error: 'SSH connection succeeded but no data returned', + suggestion: 'Check repository access permissions' + }); + } + } catch (sshError) { + console.error('SSH test error:', sshError.message); + + if (sshError.message.includes('Permission denied')) { + return res.status(403).json({ + error: 'SSH key permission denied', + details: 'The SSH key exists but GitHub rejected the connection', + suggestion: 'Verify the SSH key is added to the repository as a deploy key with read access' + }); + } else if (sshError.message.includes('Host key verification failed')) { + return res.status(403).json({ + error: 'Host key verification failed', + suggestion: 'This is normal for first-time connections. The key will be added to known_hosts automatically.' + }); + } else if (sshError.message.includes('Connection timed out')) { + return res.status(408).json({ + error: 'Connection timed out', + suggestion: 'Check your internet connection and GitHub status' + }); + } else { + return res.status(500).json({ + error: 'SSH connection failed', + details: sshError.message, + suggestion: 'Check the SSH key format and repository URL' + }); + } + } + + } catch (error) { + console.error('SSH key test error:', error); + res.status(500).json({ + error: 'Failed to test SSH key', + details: error.message + }); + } +}); + // Check for updates from GitHub router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => { try { diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 99b919c..98bf8ad 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -12,7 +12,8 @@ const Settings = () => { updateInterval: 60, autoUpdate: false, githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git', - sshKeyPath: '' + sshKeyPath: '', + useCustomSshKey: false }); const [errors, setErrors] = useState({}); const [isDirty, setIsDirty] = useState(false); @@ -46,6 +47,13 @@ const Settings = () => { checking: false, error: null }); + + const [sshTestResult, setSshTestResult] = useState({ + testing: false, + success: null, + message: null, + error: null + }); const queryClient = useQueryClient(); @@ -68,7 +76,8 @@ const Settings = () => { updateInterval: settings.updateInterval || 60, autoUpdate: settings.autoUpdate || false, githubRepoUrl: settings.githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git', - sshKeyPath: settings.sshKeyPath || '' + sshKeyPath: settings.sshKeyPath || '', + useCustomSshKey: !!settings.sshKeyPath }; console.log('Setting form data to:', newFormData); setFormData(newFormData); @@ -188,6 +197,42 @@ const Settings = () => { } }; + const testSshKey = async () => { + if (!formData.sshKeyPath || !formData.githubRepoUrl) { + setSshTestResult({ + testing: false, + success: false, + message: null, + error: 'Please enter both SSH key path and GitHub repository URL' + }); + return; + } + + setSshTestResult({ testing: true, success: null, message: null, error: null }); + + try { + const response = await versionAPI.testSshKey({ + sshKeyPath: formData.sshKeyPath, + githubRepoUrl: formData.githubRepoUrl + }); + + setSshTestResult({ + testing: false, + success: true, + message: response.data.message, + error: null + }); + } catch (error) { + console.error('SSH key test error:', error); + setSshTestResult({ + testing: false, + success: false, + message: null, + error: error.response?.data?.error || 'Failed to test SSH key' + }); + } + }; + const handleInputChange = (field, value) => { console.log(`handleInputChange: ${field} = ${value}`); setFormData(prev => { @@ -203,7 +248,16 @@ const Settings = () => { const handleSubmit = (e) => { e.preventDefault(); - updateSettingsMutation.mutate(formData); + + // Only include sshKeyPath if the toggle is enabled + const dataToSubmit = { ...formData }; + if (!dataToSubmit.useCustomSshKey) { + dataToSubmit.sshKeyPath = ''; + } + // Remove the frontend-only field + delete dataToSubmit.useCustomSshKey; + + updateSettingsMutation.mutate(dataToSubmit); }; const validateForm = () => { @@ -725,19 +779,81 @@ const Settings = () => {
- - handleInputChange('sshKeyPath', e.target.value)} - className="w-full border border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white font-mono text-sm" - placeholder="/root/.ssh/id_ed25519" - /> -

- Path to your SSH deploy key. Leave empty to auto-detect from common locations. -

+
+ { + const checked = e.target.checked; + handleInputChange('useCustomSshKey', checked); + if (!checked) { + handleInputChange('sshKeyPath', ''); + } + }} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" + /> + +
+ + {formData.useCustomSshKey && ( +
+ + handleInputChange('sshKeyPath', e.target.value)} + className="w-full border border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white font-mono text-sm" + placeholder="/root/.ssh/id_ed25519" + /> +

+ Path to your SSH deploy key. If not set, will auto-detect from common locations. +

+ +
+ + + {sshTestResult.success && ( +
+
+ +

+ {sshTestResult.message} +

+
+
+ )} + + {sshTestResult.error && ( +
+
+ +

+ {sshTestResult.error} +

+
+
+ )} +
+
+ )} + + {!formData.useCustomSshKey && ( +

+ Using auto-detection for SSH key location +

+ )}
diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 5281cdd..eacbcfd 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -182,6 +182,7 @@ export const formatDate = (date) => { export const versionAPI = { getCurrent: () => api.get('/version/current'), checkUpdates: () => api.get('/version/check-updates'), + testSshKey: (data) => api.post('/version/test-ssh-key', data), } export const formatRelativeTime = (date) => {