mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-04 14:03:17 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bdd0b5830 | ||
|
|
98cadb1ff1 | ||
|
|
42a6b7e19c | ||
|
|
e35f96d30f | ||
|
|
08f82bc795 | ||
|
|
c497c1db2a | ||
|
|
5b7e7216e8 | ||
|
|
fe5fb92e48 | ||
|
|
c8d54facb9 | ||
|
|
f97b300158 | ||
|
|
17ffa48158 | ||
|
|
16821d6b5e |
@@ -6,7 +6,7 @@
|
||||
# Configuration
|
||||
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
|
||||
API_VERSION="v1"
|
||||
AGENT_VERSION="1.2.3"
|
||||
AGENT_VERSION="1.2.4"
|
||||
CONFIG_FILE="/etc/patchmon/agent.conf"
|
||||
CREDENTIALS_FILE="/etc/patchmon/credentials"
|
||||
LOG_FILE="/var/log/patchmon-agent.log"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon-backend",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.4",
|
||||
"description": "Backend API for Linux Patch Monitoring System",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "settings" ADD COLUMN "ssh_key_path" TEXT;
|
||||
@@ -180,6 +180,7 @@ model Settings {
|
||||
updateInterval Int @map("update_interval") @default(60) // Update interval in minutes
|
||||
autoUpdate Boolean @map("auto_update") @default(false) // Enable automatic agent updates
|
||||
githubRepoUrl String @map("github_repo_url") @default("git@github.com:9technologygroup/patchmon.net.git") // GitHub repository URL for version checking
|
||||
sshKeyPath String? @map("ssh_key_path") // Optional SSH key path for deploy key authentication
|
||||
createdAt DateTime @map("created_at") @default(now())
|
||||
updatedAt DateTime @map("updated_at") @updatedAt
|
||||
|
||||
|
||||
@@ -123,7 +123,16 @@ router.put('/', authenticateToken, requireManageSettings, [
|
||||
body('frontendUrl').isLength({ min: 1 }).withMessage('Frontend URL is required'),
|
||||
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('githubRepoUrl').optional().isLength({ min: 1 }).withMessage('GitHub repo URL 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);
|
||||
@@ -133,8 +142,8 @@ router.put('/', authenticateToken, requireManageSettings, [
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl } = req.body;
|
||||
console.log('Extracted values:', { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl });
|
||||
const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, sshKeyPath } = req.body;
|
||||
console.log('Extracted values:', { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, sshKeyPath });
|
||||
|
||||
// Construct server URL from components
|
||||
const serverUrl = `${serverProtocol}://${serverHost}:${serverPort}`;
|
||||
@@ -165,7 +174,8 @@ router.put('/', authenticateToken, requireManageSettings, [
|
||||
frontendUrl,
|
||||
updateInterval: updateInterval || 60,
|
||||
autoUpdate: autoUpdate || false,
|
||||
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git'
|
||||
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
|
||||
sshKeyPath: sshKeyPath || null
|
||||
}
|
||||
});
|
||||
console.log('Settings updated successfully:', settings);
|
||||
@@ -186,7 +196,8 @@ router.put('/', authenticateToken, requireManageSettings, [
|
||||
frontendUrl,
|
||||
updateInterval: updateInterval || 60,
|
||||
autoUpdate: autoUpdate || false,
|
||||
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git'
|
||||
githubRepoUrl: githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
|
||||
sshKeyPath: sshKeyPath || null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
309
backend/src/routes/versionRoutes.js
Normal file
309
backend/src/routes/versionRoutes.js
Normal file
@@ -0,0 +1,309 @@
|
||||
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.4';
|
||||
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
// 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 with deploy keys (secure approach)
|
||||
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
|
||||
|
||||
try {
|
||||
let sshKeyPath = null;
|
||||
|
||||
// First, try to use the configured SSH key path from settings
|
||||
if (settings.sshKeyPath) {
|
||||
try {
|
||||
require('fs').accessSync(settings.sshKeyPath);
|
||||
sshKeyPath = settings.sshKeyPath;
|
||||
console.log(`Using configured SSH key at: ${sshKeyPath}`);
|
||||
} catch (e) {
|
||||
console.warn(`Configured SSH key path not accessible: ${settings.sshKeyPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If no configured path or it's not accessible, try common locations
|
||||
if (!sshKeyPath) {
|
||||
const possibleKeyPaths = [
|
||||
'/root/.ssh/id_ed25519', // Root user (if service runs as root)
|
||||
'/root/.ssh/id_rsa', // Root user RSA key
|
||||
'/home/patchmon/.ssh/id_ed25519', // PatchMon user
|
||||
'/home/patchmon/.ssh/id_rsa', // PatchMon user RSA key
|
||||
'/var/www/.ssh/id_ed25519', // Web user
|
||||
'/var/www/.ssh/id_rsa' // Web user RSA key
|
||||
];
|
||||
|
||||
for (const path of possibleKeyPaths) {
|
||||
try {
|
||||
require('fs').accessSync(path);
|
||||
sshKeyPath = path;
|
||||
console.log(`Found SSH key at: ${path}`);
|
||||
break;
|
||||
} catch (e) {
|
||||
// Key not found at this path, try next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sshKeyPath) {
|
||||
throw new Error('No SSH deploy key found. Please configure the SSH key path in settings or ensure a deploy key is installed in one of the expected locations.');
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes`
|
||||
};
|
||||
|
||||
// Fetch the latest tag using SSH with deploy key
|
||||
const { stdout: latestTag } = await execAsync(
|
||||
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
|
||||
{
|
||||
timeout: 10000,
|
||||
env: env
|
||||
}
|
||||
);
|
||||
|
||||
const latestVersion = latestTag.trim().replace('v', ''); // Remove 'v' prefix
|
||||
const currentVersion = '1.2.4';
|
||||
|
||||
// Simple version comparison (assumes semantic versioning)
|
||||
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
||||
|
||||
res.json({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
isUpdateAvailable,
|
||||
latestRelease: {
|
||||
tagName: latestTag.trim(),
|
||||
version: latestVersion,
|
||||
repository: `${owner}/${repo}`,
|
||||
sshUrl: sshRepoUrl,
|
||||
sshKeyUsed: sshKeyPath
|
||||
}
|
||||
});
|
||||
|
||||
} catch (sshError) {
|
||||
console.error('SSH Git error:', sshError.message);
|
||||
|
||||
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 deploy key is properly configured and has access to the repository. Check that the key has read access to the repository.'
|
||||
});
|
||||
}
|
||||
|
||||
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 with the deploy key.'
|
||||
});
|
||||
}
|
||||
|
||||
if (sshError.message.includes('No SSH deploy key found')) {
|
||||
return res.status(400).json({
|
||||
error: 'No SSH deploy key found',
|
||||
suggestion: 'Please install a deploy key in one of the expected locations: /root/.ssh/, /home/patchmon/.ssh/, or /var/www/.ssh/'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'Failed to fetch repository information',
|
||||
details: sshError.message,
|
||||
suggestion: 'Check deploy 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 @@
|
||||
{
|
||||
"name": "patchmon-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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({
|
||||
@@ -11,7 +11,9 @@ const Settings = () => {
|
||||
frontendUrl: 'http://localhost:3000',
|
||||
updateInterval: 60,
|
||||
autoUpdate: false,
|
||||
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git'
|
||||
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git',
|
||||
sshKeyPath: '',
|
||||
useCustomSshKey: false
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
@@ -37,6 +39,22 @@ const Settings = () => {
|
||||
isDefault: false
|
||||
});
|
||||
|
||||
// Version checking state
|
||||
const [versionInfo, setVersionInfo] = useState({
|
||||
currentVersion: '1.2.4',
|
||||
latestVersion: null,
|
||||
isUpdateAvailable: false,
|
||||
checking: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const [sshTestResult, setSshTestResult] = useState({
|
||||
testing: false,
|
||||
success: null,
|
||||
message: null,
|
||||
error: null
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch current settings
|
||||
@@ -57,7 +75,9 @@ const Settings = () => {
|
||||
frontendUrl: settings.frontendUrl || 'http://localhost:3000',
|
||||
updateInterval: settings.updateInterval || 60,
|
||||
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 || '',
|
||||
useCustomSshKey: !!settings.sshKeyPath
|
||||
};
|
||||
console.log('Setting form data to:', newFormData);
|
||||
setFormData(newFormData);
|
||||
@@ -80,12 +100,15 @@ const Settings = () => {
|
||||
queryClient.invalidateQueries(['settings']);
|
||||
// Update form data with the returned data
|
||||
setFormData({
|
||||
serverProtocol: data.serverProtocol || 'http',
|
||||
serverHost: data.serverHost || 'localhost',
|
||||
serverPort: data.serverPort || 3001,
|
||||
frontendUrl: data.frontendUrl || 'http://localhost:3000',
|
||||
updateInterval: data.updateInterval || 60,
|
||||
autoUpdate: data.autoUpdate || false
|
||||
serverProtocol: data.settings?.serverProtocol || data.serverProtocol || 'http',
|
||||
serverHost: data.settings?.serverHost || data.serverHost || 'localhost',
|
||||
serverPort: data.settings?.serverPort || data.serverPort || 3001,
|
||||
frontendUrl: data.settings?.frontendUrl || data.frontendUrl || 'http://localhost:3000',
|
||||
updateInterval: data.settings?.updateInterval || data.updateInterval || 60,
|
||||
autoUpdate: data.settings?.autoUpdate || data.autoUpdate || false,
|
||||
githubRepoUrl: data.settings?.githubRepoUrl || data.githubRepoUrl || 'git@github.com:9technologygroup/patchmon.net.git',
|
||||
sshKeyPath: data.settings?.sshKeyPath || data.sshKeyPath || '',
|
||||
useCustomSshKey: !!(data.settings?.sshKeyPath || data.sshKeyPath)
|
||||
});
|
||||
setIsDirty(false);
|
||||
setErrors({});
|
||||
@@ -152,6 +175,67 @@ 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 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 => {
|
||||
@@ -167,7 +251,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 = () => {
|
||||
@@ -199,7 +292,16 @@ const Settings = () => {
|
||||
console.log('Saving settings:', formData);
|
||||
if (validateForm()) {
|
||||
console.log('Validation passed, calling mutation');
|
||||
updateSettingsMutation.mutate(formData);
|
||||
|
||||
// Prepare data for submission
|
||||
const dataToSubmit = { ...formData };
|
||||
if (!dataToSubmit.useCustomSshKey) {
|
||||
dataToSubmit.sshKeyPath = '';
|
||||
}
|
||||
// Remove the frontend-only field
|
||||
delete dataToSubmit.useCustomSshKey;
|
||||
|
||||
updateSettingsMutation.mutate(dataToSubmit);
|
||||
} else {
|
||||
console.log('Validation failed:', errors);
|
||||
}
|
||||
@@ -688,13 +790,91 @@ const Settings = () => {
|
||||
</p>
|
||||
</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>
|
||||
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
|
||||
SSH Key Path
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.sshKeyPath || ''}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||
Path to your SSH deploy key. If not set, will auto-detect from common locations.
|
||||
</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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-600">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<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,52 +882,91 @@ 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...');
|
||||
}}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Check for Updates
|
||||
</button>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={checkForUpdates}
|
||||
disabled={versionInfo.checking}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{versionInfo.checking ? 'Checking...' : 'Check for Updates'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Save Button for Version Settings */}
|
||||
<button
|
||||
onClick={() => {
|
||||
// TODO: Implement update notification
|
||||
console.log('Enable update notifications');
|
||||
}}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={!isDirty || updateSettingsMutation.isPending}
|
||||
className={`inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white ${
|
||||
!isDirty || updateSettingsMutation.isPending
|
||||
? 'bg-secondary-400 cursor-not-allowed'
|
||||
: 'bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500'
|
||||
}`}
|
||||
>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
Enable Notifications
|
||||
{updateSettingsMutation.isPending ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Success Message for Version Settings */}
|
||||
{updateSettingsMutation.isSuccess && (
|
||||
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 dark:text-green-300" />
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-green-700 dark:text-green-300">Settings saved successfully!</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,13 @@ 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'),
|
||||
testSshKey: (data) => api.post('/version/test-ssh-key', data),
|
||||
}
|
||||
|
||||
export const formatRelativeTime = (date) => {
|
||||
const now = new Date()
|
||||
const diff = now - new Date(date)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.4",
|
||||
"description": "Linux Patch Monitoring System",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user