mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-22 23:32:03 +00:00
feat: Implement SSH-based version checking for private repositories
- Replace HTTPS GitHub API calls with SSH git commands - Use existing deploy key for private repository access - Add proper error handling for SSH authentication issues - Support fetching latest tags via git ls-remote - Maintain compatibility with private repositories using deploy keys This allows the version checking system to work with private repositories that have SSH deploy keys configured, using the same authentication as the local git operations.
This commit is contained in:
160
backend/src/routes/versionRoutes.js
Normal file
160
backend/src/routes/versionRoutes.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const express = require('express');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { requireManageSettings } = require('../middleware/permissions');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get current version info
|
||||
router.get('/current', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
// For now, return hardcoded version - this should match your agent version
|
||||
const currentVersion = '1.2.3';
|
||||
|
||||
res.json({
|
||||
version: currentVersion,
|
||||
buildDate: new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting current version:', error);
|
||||
res.status(500).json({ error: 'Failed to get current version' });
|
||||
}
|
||||
});
|
||||
|
||||
// Check for updates from GitHub
|
||||
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
|
||||
try {
|
||||
// Get GitHub repo URL from settings
|
||||
const settings = await prisma.settings.findFirst();
|
||||
if (!settings || !settings.githubRepoUrl) {
|
||||
return res.status(400).json({ error: 'GitHub repository URL not configured' });
|
||||
}
|
||||
|
||||
// Extract owner and repo from GitHub URL
|
||||
// Support both SSH and HTTPS formats:
|
||||
// git@github.com:owner/repo.git
|
||||
// https://github.com/owner/repo.git
|
||||
const repoUrl = settings.githubRepoUrl;
|
||||
let owner, repo;
|
||||
|
||||
if (repoUrl.includes('git@github.com:')) {
|
||||
const match = repoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/);
|
||||
if (match) {
|
||||
[, owner, repo] = match;
|
||||
}
|
||||
} else if (repoUrl.includes('github.com/')) {
|
||||
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
||||
if (match) {
|
||||
[, owner, repo] = match;
|
||||
}
|
||||
}
|
||||
|
||||
if (!owner || !repo) {
|
||||
return res.status(400).json({ error: 'Invalid GitHub repository URL format' });
|
||||
}
|
||||
|
||||
// Use SSH to fetch latest tag from private repository
|
||||
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
|
||||
|
||||
try {
|
||||
// Fetch the latest tag using SSH
|
||||
const { stdout: latestTag } = await execAsync(
|
||||
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^\\{\\}//'`,
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
const latestVersion = latestTag.trim().replace('v', ''); // Remove 'v' prefix
|
||||
const currentVersion = '1.2.3';
|
||||
|
||||
// Simple version comparison (assumes semantic versioning)
|
||||
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
||||
|
||||
// Get additional tag information
|
||||
let tagInfo = {};
|
||||
try {
|
||||
const { stdout: tagDetails } = await execAsync(
|
||||
`git ls-remote --tags ${sshRepoUrl} | grep "${latestTag.trim()}" | head -n 1`,
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Extract commit hash and other details if needed
|
||||
const parts = tagDetails.trim().split('\t');
|
||||
if (parts.length >= 2) {
|
||||
tagInfo.commitHash = parts[0];
|
||||
}
|
||||
} catch (tagDetailError) {
|
||||
console.warn('Could not fetch tag details:', tagDetailError.message);
|
||||
}
|
||||
|
||||
res.json({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
isUpdateAvailable,
|
||||
latestRelease: {
|
||||
tagName: latestTag.trim(),
|
||||
version: latestVersion,
|
||||
commitHash: tagInfo.commitHash,
|
||||
repository: `${owner}/${repo}`,
|
||||
sshUrl: sshRepoUrl
|
||||
}
|
||||
});
|
||||
|
||||
} catch (sshError) {
|
||||
console.error('SSH Git error:', sshError.message);
|
||||
|
||||
// Check if it's a permission/access issue
|
||||
if (sshError.message.includes('Permission denied') || sshError.message.includes('Host key verification failed')) {
|
||||
return res.status(403).json({
|
||||
error: 'SSH access denied to repository',
|
||||
suggestion: 'Ensure your SSH key is properly configured and has access to the repository. Check your ~/.ssh/config and known_hosts.'
|
||||
});
|
||||
}
|
||||
|
||||
if (sshError.message.includes('not found') || sshError.message.includes('does not exist')) {
|
||||
return res.status(404).json({
|
||||
error: 'Repository not found',
|
||||
suggestion: 'Check that the repository URL is correct and accessible.'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'Failed to fetch repository information',
|
||||
details: sshError.message,
|
||||
suggestion: 'Check SSH key configuration and repository access permissions.'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to check for updates',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Simple version comparison function
|
||||
function compareVersions(version1, version2) {
|
||||
const v1Parts = version1.split('.').map(Number);
|
||||
const v2Parts = version2.split('.').map(Number);
|
||||
|
||||
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const v1Part = v1Parts[i] || 0;
|
||||
const v2Part = v2Parts[i] || 0;
|
||||
|
||||
if (v1Part > v2Part) return 1;
|
||||
if (v1Part < v2Part) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
module.exports = router;
|
@@ -16,6 +16,7 @@ const permissionsRoutes = require('./routes/permissionsRoutes');
|
||||
const settingsRoutes = require('./routes/settingsRoutes');
|
||||
const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes');
|
||||
const repositoryRoutes = require('./routes/repositoryRoutes');
|
||||
const versionRoutes = require('./routes/versionRoutes');
|
||||
|
||||
// Initialize Prisma client
|
||||
const prisma = new PrismaClient();
|
||||
@@ -134,6 +135,7 @@ app.use(`/api/${apiVersion}/permissions`, permissionsRoutes);
|
||||
app.use(`/api/${apiVersion}/settings`, settingsRoutes);
|
||||
app.use(`/api/${apiVersion}/dashboard-preferences`, dashboardPreferencesRoutes);
|
||||
app.use(`/api/${apiVersion}/repositories`, repositoryRoutes);
|
||||
app.use(`/api/${apiVersion}/version`, versionRoutes);
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Save, Server, Globe, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon } from 'lucide-react';
|
||||
import { settingsAPI, agentVersionAPI } from '../utils/api';
|
||||
import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api';
|
||||
|
||||
const Settings = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -37,6 +37,15 @@ const Settings = () => {
|
||||
isDefault: false
|
||||
});
|
||||
|
||||
// Version checking state
|
||||
const [versionInfo, setVersionInfo] = useState({
|
||||
currentVersion: '1.2.3',
|
||||
latestVersion: null,
|
||||
isUpdateAvailable: false,
|
||||
checking: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch current settings
|
||||
@@ -152,6 +161,31 @@ const Settings = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Version checking functions
|
||||
const checkForUpdates = async () => {
|
||||
setVersionInfo(prev => ({ ...prev, checking: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await versionAPI.checkUpdates();
|
||||
const data = response.data;
|
||||
|
||||
setVersionInfo({
|
||||
currentVersion: data.currentVersion,
|
||||
latestVersion: data.latestVersion,
|
||||
isUpdateAvailable: data.isUpdateAvailable,
|
||||
checking: false,
|
||||
error: null
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Version check error:', error);
|
||||
setVersionInfo(prev => ({
|
||||
...prev,
|
||||
checking: false,
|
||||
error: error.response?.data?.error || 'Failed to check for updates'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
console.log(`handleInputChange: ${field} = ${value}`);
|
||||
setFormData(prev => {
|
||||
@@ -694,7 +728,7 @@ const Settings = () => {
|
||||
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">Current Version</span>
|
||||
</div>
|
||||
<span className="text-lg font-mono text-secondary-900 dark:text-white">1.2.3</span>
|
||||
<span className="text-lg font-mono text-secondary-900 dark:text-white">{versionInfo.currentVersion}</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-600">
|
||||
@@ -702,20 +736,29 @@ const Settings = () => {
|
||||
<Download className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">Latest Version</span>
|
||||
</div>
|
||||
<span className="text-lg font-mono text-secondary-900 dark:text-white">Checking...</span>
|
||||
<span className="text-lg font-mono text-secondary-900 dark:text-white">
|
||||
{versionInfo.checking ? (
|
||||
<span className="text-blue-600 dark:text-blue-400">Checking...</span>
|
||||
) : versionInfo.latestVersion ? (
|
||||
<span className={versionInfo.isUpdateAvailable ? 'text-orange-600 dark:text-orange-400' : 'text-green-600 dark:text-green-400'}>
|
||||
{versionInfo.latestVersion}
|
||||
{versionInfo.isUpdateAvailable && ' (Update Available!)'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-secondary-500 dark:text-secondary-400">Not checked</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
// TODO: Implement version check
|
||||
console.log('Checking for updates...');
|
||||
}}
|
||||
onClick={checkForUpdates}
|
||||
disabled={versionInfo.checking}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Check for Updates
|
||||
{versionInfo.checking ? 'Checking...' : 'Check for Updates'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -729,25 +772,28 @@ const Settings = () => {
|
||||
Enable Notifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{versionInfo.error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<AlertCircle className="h-5 w-5 text-red-400 dark:text-red-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">Version Check Failed</h3>
|
||||
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
|
||||
{versionInfo.error}
|
||||
</p>
|
||||
{versionInfo.error.includes('private') && (
|
||||
<p className="mt-2 text-xs text-red-600 dark:text-red-400">
|
||||
For private repositories, you may need to configure GitHub authentication or make the repository public.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<AlertCircle className="h-5 w-5 text-amber-400 dark:text-amber-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">Setup Instructions</h3>
|
||||
<div className="mt-2 text-sm text-amber-700 dark:text-amber-300">
|
||||
<p className="mb-2">To enable version checking, you need to:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 ml-4">
|
||||
<li>Create a version tag (e.g., v1.2.3) in your GitHub repository</li>
|
||||
<li>Ensure the repository is publicly accessible or configure access tokens</li>
|
||||
<li>Click "Check for Updates" to verify the connection</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -178,6 +178,12 @@ export const formatDate = (date) => {
|
||||
return new Date(date).toLocaleString()
|
||||
}
|
||||
|
||||
// Version API
|
||||
export const versionAPI = {
|
||||
getCurrent: () => api.get('/version/current'),
|
||||
checkUpdates: () => api.get('/version/check-updates'),
|
||||
}
|
||||
|
||||
export const formatRelativeTime = (date) => {
|
||||
const now = new Date()
|
||||
const diff = now - new Date(date)
|
||||
|
Reference in New Issue
Block a user