mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-01 20:44:09 +00:00
added server.js for frontend when not using nginx in deployment
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "settings" ADD COLUMN "last_update_check" TIMESTAMP(3),
|
||||
ADD COLUMN "latest_version" TEXT,
|
||||
ADD COLUMN "update_available" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -187,6 +187,9 @@ model Settings {
|
||||
githubRepoUrl String @map("github_repo_url") @default("git@github.com:9technologygroup/patchmon.net.git") // GitHub repository URL for version checking
|
||||
repositoryType String @map("repository_type") @default("public") // "public" or "private"
|
||||
sshKeyPath String? @map("ssh_key_path") // Optional SSH key path for deploy key authentication
|
||||
lastUpdateCheck DateTime? @map("last_update_check") // When the system last checked for updates
|
||||
updateAvailable Boolean @map("update_available") @default(false) // Whether an update is available
|
||||
latestVersion String? @map("latest_version") // Latest available version
|
||||
createdAt DateTime @map("created_at") @default(now())
|
||||
updatedAt DateTime @map("updated_at") @updatedAt
|
||||
|
||||
|
||||
@@ -142,202 +142,36 @@ router.post('/test-ssh-key', authenticateToken, requireManageSettings, async (re
|
||||
// Check for updates from GitHub
|
||||
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
|
||||
try {
|
||||
// Get GitHub repo URL from settings
|
||||
// Get cached update information from settings
|
||||
const settings = await prisma.settings.findFirst();
|
||||
console.log('Settings retrieved for version check:', {
|
||||
id: settings?.id,
|
||||
githubRepoUrl: settings?.githubRepoUrl,
|
||||
repositoryType: settings?.repositoryType
|
||||
|
||||
if (!settings) {
|
||||
return res.status(400).json({ error: 'Settings not found' });
|
||||
}
|
||||
|
||||
const currentVersion = '1.2.4';
|
||||
const latestVersion = settings.latestVersion || currentVersion;
|
||||
const isUpdateAvailable = settings.updateAvailable || false;
|
||||
const lastUpdateCheck = settings.lastUpdateCheck;
|
||||
|
||||
res.json({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
isUpdateAvailable,
|
||||
lastUpdateCheck,
|
||||
repositoryType: settings.repositoryType || 'public',
|
||||
latestRelease: {
|
||||
tagName: latestVersion ? `v${latestVersion}` : null,
|
||||
version: latestVersion,
|
||||
repository: settings.githubRepoUrl ? settings.githubRepoUrl.split('/').slice(-2).join('/') : null,
|
||||
accessMethod: settings.repositoryType === 'private' ? 'ssh' : 'api'
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
console.log('Using repository URL:', repoUrl);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Extracted owner and repo:', { owner, repo });
|
||||
|
||||
if (!owner || !repo) {
|
||||
return res.status(400).json({ error: 'Invalid GitHub repository URL format' });
|
||||
}
|
||||
|
||||
// Determine repository type and set up appropriate access method
|
||||
const repositoryType = settings.repositoryType || 'public';
|
||||
const isPrivate = repositoryType === 'private';
|
||||
|
||||
let latestTag;
|
||||
|
||||
if (isPrivate) {
|
||||
// Use SSH with deploy keys for private repositories
|
||||
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: sshLatestTag } = await execAsync(
|
||||
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
|
||||
{
|
||||
timeout: 10000,
|
||||
env: env
|
||||
}
|
||||
);
|
||||
|
||||
latestTag = sshLatestTag;
|
||||
} catch (sshError) {
|
||||
console.error('SSH Git error:', sshError.message);
|
||||
throw sshError;
|
||||
}
|
||||
} else {
|
||||
// Use GitHub API for public repositories (no authentication required)
|
||||
try {
|
||||
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||
|
||||
const response = await fetch(httpsRepoUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'PatchMon-Server/1.2.4'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const releaseData = await response.json();
|
||||
latestTag = releaseData.tag_name;
|
||||
} catch (apiError) {
|
||||
console.error('GitHub API error:', apiError.message);
|
||||
throw apiError;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
repositoryType,
|
||||
latestRelease: {
|
||||
tagName: latestTag.trim(),
|
||||
version: latestVersion,
|
||||
repository: `${owner}/${repo}`,
|
||||
accessMethod: isPrivate ? 'ssh' : 'api',
|
||||
...(isPrivate && { sshUrl: `git@github.com:${owner}/${repo}.git`, sshKeyUsed: settings.sshKeyPath })
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Version check error:', error.message);
|
||||
|
||||
if (error.message.includes('Permission denied') || error.message.includes('Host key verification failed')) {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied to repository',
|
||||
suggestion: isPrivate
|
||||
? 'Ensure your deploy key is properly configured and has access to the repository. Check that the key has read access to the repository.'
|
||||
: 'Check that the repository URL is correct and the repository is public.'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('not found') || error.message.includes('does not exist')) {
|
||||
return res.status(404).json({
|
||||
error: 'Repository not found',
|
||||
suggestion: 'Check that the repository URL is correct and accessible.'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.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/'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('GitHub API error')) {
|
||||
return res.status(500).json({
|
||||
error: 'Failed to fetch repository information via GitHub API',
|
||||
details: error.message,
|
||||
suggestion: 'Check that the repository URL is correct and the repository is public.'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'Failed to fetch repository information',
|
||||
details: error.message,
|
||||
suggestion: isPrivate
|
||||
? 'Check deploy key configuration and repository access permissions.'
|
||||
: 'Check repository URL and ensure it is accessible.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting update information:', error);
|
||||
res.status(500).json({ error: 'Failed to get update information' });
|
||||
}
|
||||
});
|
||||
|
||||
// Simple version comparison function
|
||||
|
||||
@@ -18,6 +18,7 @@ const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes'
|
||||
const repositoryRoutes = require('./routes/repositoryRoutes');
|
||||
const versionRoutes = require('./routes/versionRoutes');
|
||||
const tfaRoutes = require('./routes/tfaRoutes');
|
||||
const updateScheduler = require('./services/updateScheduler');
|
||||
|
||||
// Initialize Prisma client
|
||||
const prisma = new PrismaClient();
|
||||
@@ -160,6 +161,7 @@ process.on('SIGTERM', async () => {
|
||||
if (process.env.ENABLE_LOGGING === 'true') {
|
||||
logger.info('SIGTERM received, shutting down gracefully');
|
||||
}
|
||||
updateScheduler.stop();
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -168,6 +170,7 @@ process.on('SIGINT', async () => {
|
||||
if (process.env.ENABLE_LOGGING === 'true') {
|
||||
logger.info('SIGINT received, shutting down gracefully');
|
||||
}
|
||||
updateScheduler.stop();
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -178,6 +181,9 @@ app.listen(PORT, () => {
|
||||
logger.info(`Server running on port ${PORT}`);
|
||||
logger.info(`Environment: ${process.env.NODE_ENV}`);
|
||||
}
|
||||
|
||||
// Start update scheduler
|
||||
updateScheduler.start();
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
247
backend/src/services/updateScheduler.js
Normal file
247
backend/src/services/updateScheduler.js
Normal file
@@ -0,0 +1,247 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
class UpdateScheduler {
|
||||
constructor() {
|
||||
this.isRunning = false;
|
||||
this.intervalId = null;
|
||||
this.checkInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
}
|
||||
|
||||
// Start the scheduler
|
||||
start() {
|
||||
if (this.isRunning) {
|
||||
console.log('Update scheduler is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔄 Starting update scheduler...');
|
||||
this.isRunning = true;
|
||||
|
||||
// Run initial check
|
||||
this.checkForUpdates();
|
||||
|
||||
// Schedule regular checks
|
||||
this.intervalId = setInterval(() => {
|
||||
this.checkForUpdates();
|
||||
}, this.checkInterval);
|
||||
|
||||
console.log(`✅ Update scheduler started - checking every ${this.checkInterval / (60 * 60 * 1000)} hours`);
|
||||
}
|
||||
|
||||
// Stop the scheduler
|
||||
stop() {
|
||||
if (!this.isRunning) {
|
||||
console.log('Update scheduler is not running');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🛑 Stopping update scheduler...');
|
||||
this.isRunning = false;
|
||||
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
|
||||
console.log('✅ Update scheduler stopped');
|
||||
}
|
||||
|
||||
// Check for updates
|
||||
async checkForUpdates() {
|
||||
try {
|
||||
console.log('🔍 Checking for updates...');
|
||||
|
||||
// Get settings
|
||||
const settings = await prisma.settings.findFirst();
|
||||
if (!settings || !settings.githubRepoUrl) {
|
||||
console.log('⚠️ No GitHub repository configured, skipping update check');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract owner and repo from GitHub URL
|
||||
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\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
|
||||
if (match) {
|
||||
[, owner, repo] = match;
|
||||
}
|
||||
}
|
||||
|
||||
if (!owner || !repo) {
|
||||
console.log('⚠️ Could not parse GitHub repository URL, skipping update check');
|
||||
return;
|
||||
}
|
||||
|
||||
let latestVersion;
|
||||
const isPrivate = settings.repositoryType === 'private';
|
||||
|
||||
if (isPrivate) {
|
||||
// Use SSH for private repositories
|
||||
latestVersion = await this.checkPrivateRepo(settings, owner, repo);
|
||||
} else {
|
||||
// Use GitHub API for public repositories
|
||||
latestVersion = await this.checkPublicRepo(owner, repo);
|
||||
}
|
||||
|
||||
if (!latestVersion) {
|
||||
console.log('⚠️ Could not determine latest version, skipping update check');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = '1.2.4';
|
||||
const isUpdateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||
|
||||
// Update settings with check results
|
||||
await prisma.settings.update({
|
||||
where: { id: settings.id },
|
||||
data: {
|
||||
lastUpdateCheck: new Date(),
|
||||
updateAvailable: isUpdateAvailable,
|
||||
latestVersion: latestVersion
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Update check completed - Current: ${currentVersion}, Latest: ${latestVersion}, Update Available: ${isUpdateAvailable}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error checking for updates:', error.message);
|
||||
|
||||
// Update last check time even on error
|
||||
try {
|
||||
const settings = await prisma.settings.findFirst();
|
||||
if (settings) {
|
||||
await prisma.settings.update({
|
||||
where: { id: settings.id },
|
||||
data: {
|
||||
lastUpdateCheck: new Date(),
|
||||
updateAvailable: false
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (updateError) {
|
||||
console.error('❌ Error updating last check time:', updateError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check private repository using SSH
|
||||
async checkPrivateRepo(settings, owner, repo) {
|
||||
try {
|
||||
let sshKeyPath = settings.sshKeyPath;
|
||||
|
||||
// Try to find SSH key if not configured
|
||||
if (!sshKeyPath) {
|
||||
const possibleKeyPaths = [
|
||||
'/root/.ssh/id_ed25519',
|
||||
'/root/.ssh/id_rsa',
|
||||
'/home/patchmon/.ssh/id_ed25519',
|
||||
'/home/patchmon/.ssh/id_rsa',
|
||||
'/var/www/.ssh/id_ed25519',
|
||||
'/var/www/.ssh/id_rsa'
|
||||
];
|
||||
|
||||
for (const path of possibleKeyPaths) {
|
||||
try {
|
||||
require('fs').accessSync(path);
|
||||
sshKeyPath = path;
|
||||
break;
|
||||
} catch (e) {
|
||||
// Key not found at this path, try next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sshKeyPath) {
|
||||
throw new Error('No SSH deploy key found');
|
||||
}
|
||||
|
||||
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
|
||||
const env = {
|
||||
...process.env,
|
||||
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes`
|
||||
};
|
||||
|
||||
const { stdout: sshLatestTag } = await execAsync(
|
||||
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
|
||||
{
|
||||
timeout: 10000,
|
||||
env: env
|
||||
}
|
||||
);
|
||||
|
||||
return sshLatestTag.trim().replace('v', '');
|
||||
} catch (error) {
|
||||
console.error('SSH Git error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Check public repository using GitHub API
|
||||
async checkPublicRepo(owner, repo) {
|
||||
try {
|
||||
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||
|
||||
const response = await fetch(httpsRepoUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'PatchMon-Server/1.2.4'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const releaseData = await response.json();
|
||||
return releaseData.tag_name.replace('v', '');
|
||||
} catch (error) {
|
||||
console.error('GitHub API error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare version strings (semantic versioning)
|
||||
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;
|
||||
}
|
||||
|
||||
// Get scheduler status
|
||||
getStatus() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
checkInterval: this.checkInterval,
|
||||
nextCheck: this.isRunning ? new Date(Date.now() + this.checkInterval) : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const updateScheduler = new UpdateScheduler();
|
||||
|
||||
module.exports = updateScheduler;
|
||||
Reference in New Issue
Block a user