mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-06 15:03:26 +00:00
added deploy key custom ssh path
This commit is contained in:
@@ -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('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('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('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) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
console.log('Settings update request body:', req.body);
|
console.log('Settings update request body:', req.body);
|
||||||
|
|||||||
@@ -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
|
// Check for updates from GitHub
|
||||||
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
|
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const Settings = () => {
|
|||||||
updateInterval: 60,
|
updateInterval: 60,
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git',
|
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git',
|
||||||
sshKeyPath: ''
|
sshKeyPath: '',
|
||||||
|
useCustomSshKey: false
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
@@ -47,6 +48,13 @@ const Settings = () => {
|
|||||||
error: null
|
error: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [sshTestResult, setSshTestResult] = useState({
|
||||||
|
testing: false,
|
||||||
|
success: null,
|
||||||
|
message: null,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Fetch current settings
|
// Fetch current settings
|
||||||
@@ -68,7 +76,8 @@ const Settings = () => {
|
|||||||
updateInterval: settings.updateInterval || 60,
|
updateInterval: settings.updateInterval || 60,
|
||||||
autoUpdate: settings.autoUpdate || false,
|
autoUpdate: settings.autoUpdate || false,
|
||||||
githubRepoUrl: settings.githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
|
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);
|
console.log('Setting form data to:', newFormData);
|
||||||
setFormData(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) => {
|
const handleInputChange = (field, value) => {
|
||||||
console.log(`handleInputChange: ${field} = ${value}`);
|
console.log(`handleInputChange: ${field} = ${value}`);
|
||||||
setFormData(prev => {
|
setFormData(prev => {
|
||||||
@@ -203,7 +248,16 @@ const Settings = () => {
|
|||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
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 = () => {
|
const validateForm = () => {
|
||||||
@@ -724,9 +778,30 @@ const Settings = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="useCustomSshKey"
|
||||||
|
checked={formData.useCustomSshKey}
|
||||||
|
onChange={(e) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<label htmlFor="useCustomSshKey" className="text-sm font-medium text-secondary-700 dark:text-secondary-200">
|
||||||
|
Set custom SSH key path
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.useCustomSshKey && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
|
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
|
||||||
SSH Key Path (Optional)
|
SSH Key Path
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -736,8 +811,49 @@ const Settings = () => {
|
|||||||
placeholder="/root/.ssh/id_ed25519"
|
placeholder="/root/.ssh/id_ed25519"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
Path to your SSH deploy key. Leave empty to auto-detect from common locations.
|
Path to your SSH deploy key. If not set, will auto-detect from common locations.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={testSshKey}
|
||||||
|
disabled={sshTestResult.testing || !formData.sshKeyPath || !formData.githubRepoUrl}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{sshTestResult.testing ? 'Testing...' : 'Test SSH Key'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{sshTestResult.success && (
|
||||||
|
<div className="mt-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400 mr-2" />
|
||||||
|
<p className="text-sm text-green-800 dark:text-green-200">
|
||||||
|
{sshTestResult.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sshTestResult.error && (
|
||||||
|
<div className="mt-2 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mr-2" />
|
||||||
|
<p className="text-sm text-red-800 dark:text-red-200">
|
||||||
|
{sshTestResult.error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!formData.useCustomSshKey && (
|
||||||
|
<p className="text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
|
Using auto-detection for SSH key location
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export const formatDate = (date) => {
|
|||||||
export const versionAPI = {
|
export const versionAPI = {
|
||||||
getCurrent: () => api.get('/version/current'),
|
getCurrent: () => api.get('/version/current'),
|
||||||
checkUpdates: () => api.get('/version/check-updates'),
|
checkUpdates: () => api.get('/version/check-updates'),
|
||||||
|
testSshKey: (data) => api.post('/version/test-ssh-key', data),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatRelativeTime = (date) => {
|
export const formatRelativeTime = (date) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user